thyscript 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/bin/thy-node.js +332 -0
  2. package/bin/thy.js +828 -0
  3. package/bin/thys.js +25 -0
  4. package/examples/calculator.thy +274 -0
  5. package/examples/calculator.thy.js +628 -0
  6. package/examples/hello.thy +34 -0
  7. package/examples/login.thy +382 -0
  8. package/examples/login.thy.js +710 -0
  9. package/package.json +54 -0
  10. package/scripts/_class.js +18 -0
  11. package/scripts/_class2.js +11 -0
  12. package/scripts/_class3.js +21 -0
  13. package/scripts/_dstruct.js +20 -0
  14. package/scripts/_dstruct2.js +24 -0
  15. package/scripts/_scaffold.js +16 -0
  16. package/scripts/_scaffold2.js +16 -0
  17. package/scripts/_scaffold3.js +26 -0
  18. package/scripts/_tpl_compile.js +13 -0
  19. package/scripts/_tpl_test.js +9 -0
  20. package/scripts/_typeof.js +26 -0
  21. package/scripts/_typeof2.js +25 -0
  22. package/scripts/patch_genmethod.js +44 -0
  23. package/scripts/smoke_test.js +167 -0
  24. package/scripts/smoke_test2.js +151 -0
  25. package/scripts/test_control_panel.js +58 -0
  26. package/src/compiler/CodeGenerator.js +1449 -0
  27. package/src/compiler/JITCompiler.js +325 -0
  28. package/src/database/DatabaseEngine.js +931 -0
  29. package/src/errors/ErrorDetector.js +305 -0
  30. package/src/errors/ErrorMessages.js +497 -0
  31. package/src/index.js +169 -0
  32. package/src/lexer/Lexer.js +342 -0
  33. package/src/lexer/TokenTypes.js +1234 -0
  34. package/src/memory/MemoryManager.js +283 -0
  35. package/src/parser/AST.js +606 -0
  36. package/src/parser/Parser.js +1942 -0
  37. package/src/runtime/ThyRuntime.js +265 -0
  38. package/src/runtime/stdlib/ArrayLib.js +207 -0
  39. package/src/runtime/stdlib/FileLib.js +168 -0
  40. package/src/runtime/stdlib/MathLib.js +411 -0
  41. package/src/runtime/stdlib/StringLib.js +387 -0
  42. package/src/server/ControlPanel.js +1188 -0
  43. package/src/server/HTMLCSSEngine.js +866 -0
  44. package/src/server/SecurityEngine.js +714 -0
  45. package/src/server/ServerEngine.js +738 -0
  46. package/tests/test1_math.thy +82 -0
  47. package/tests/test2_oop.thy +133 -0
  48. package/tests/test3_server.thy +143 -0
  49. package/tests/test4_functions.thy +159 -0
@@ -0,0 +1,1942 @@
1
+ 'use strict';
2
+ /**
3
+ * ThyScript Recursive-Descent Parser
4
+ *
5
+ * Takes source text → runs the Lexer → walks the token stream → produces an AST.
6
+ *
7
+ * Operator precedence (lowest → highest):
8
+ * 1. Assignment = += -= *= /= %=
9
+ * 2. Ternary ? :
10
+ * 3. Null-coalesce ??
11
+ * 4. Logical OR || or
12
+ * 5. Logical AND && and
13
+ * 6. Bitwise OR |
14
+ * 7. Bitwise XOR ^
15
+ * 8. Bitwise AND &
16
+ * 9. Equality == != === !==
17
+ * 10. Relational < <= > >= instanceof
18
+ * 11. Bitwise shift << >>
19
+ * 12. Additive + -
20
+ * 13. Multiplicative * / %
21
+ * 14. Exponentiation ** (right-associative)
22
+ * 15. Unary prefix - ! ~ typeof not new await yield
23
+ * 16. Postfix ++ --
24
+ * 17. Call / member () . ?. []
25
+ * 18. Primary literals, identifiers, grouped, array, object, …
26
+ */
27
+
28
+ const Lexer = require('../lexer/Lexer');
29
+ const TokenType = require('../lexer/TokenTypes');
30
+ const AST = require('./AST');
31
+
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ // ParseError – a recoverable error
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+ class ParseError extends Error {
36
+ constructor(message, token, filename) {
37
+ super(message);
38
+ this.token = token;
39
+ this.filename = filename;
40
+ }
41
+ }
42
+
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+ // Parser
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+ class Parser {
47
+ /**
48
+ * @param {string|Array} sourceOrTokens – raw ThyScript source or token array
49
+ * @param {string} [filenameOrSource] – filename or raw source
50
+ * @param {string} [maybeFilename] – filename if tokens passed first
51
+ */
52
+ constructor(sourceOrTokens, filenameOrSource = '<anonymous>', maybeFilename = '<anonymous>') {
53
+ if (Array.isArray(sourceOrTokens)) {
54
+ this.tokens = sourceOrTokens;
55
+ this.source = typeof filenameOrSource === 'string' ? filenameOrSource : '';
56
+ this.filename = maybeFilename || '<anonymous>';
57
+ } else {
58
+ this.source = sourceOrTokens || '';
59
+ this.filename = filenameOrSource || '<anonymous>';
60
+ this.tokens = [];
61
+ }
62
+ this.pos = 0;
63
+ this.errors = [];
64
+ }
65
+
66
+ // ── Tokenise ──────────────────────────────────────────────────────────────
67
+ _lex() {
68
+ if (this.tokens && this.tokens.length > 0) return;
69
+ const lexer = new Lexer(this.source, this.filename);
70
+ try {
71
+ this.tokens = lexer.tokenize();
72
+ } catch (e) {
73
+ // Lexer already pushed to lexer.errors; capture and continue with
74
+ // whatever tokens were produced before the failure.
75
+ this.errors.push({ message: e.message, line: 0, col: 0 });
76
+ this.tokens = lexer.tokens;
77
+ // Ensure there is always an EOF sentinel
78
+ if (!this.tokens.length || this.tokens[this.tokens.length - 1].type !== TokenType.EOF) {
79
+ this.tokens.push({ type: TokenType.EOF, value: null, line: 0, col: 0 });
80
+ }
81
+ }
82
+ // Carry any lexer-level errors
83
+ if (lexer.errors && lexer.errors.length) {
84
+ this.errors.push(...lexer.errors);
85
+ }
86
+ }
87
+
88
+ // ── Token navigation ──────────────────────────────────────────────────────
89
+ peek(offset = 0) {
90
+ const idx = this.pos + offset;
91
+ if (idx >= this.tokens.length) return this.tokens[this.tokens.length - 1]; // EOF
92
+ return this.tokens[idx];
93
+ }
94
+
95
+ current() { return this.peek(0); }
96
+
97
+ advance() {
98
+ const tok = this.tokens[this.pos];
99
+ if (this.pos < this.tokens.length - 1) this.pos++;
100
+ return tok;
101
+ }
102
+
103
+ /** Skip NEWLINE tokens */
104
+ skipNewlines() {
105
+ while (this.current().type === TokenType.NEWLINE) this.advance();
106
+ }
107
+
108
+ check(type) { return this.current().type === type; }
109
+ checkAny(...types) { return types.includes(this.current().type); }
110
+
111
+ match(...types) {
112
+ if (types.includes(this.current().type)) { this.advance(); return true; }
113
+ return false;
114
+ }
115
+
116
+ /**
117
+ * Consume the current token if it matches `type`, otherwise record an error.
118
+ * Returns the consumed token (or the current one on failure).
119
+ */
120
+ expect(type, message) {
121
+ this.skipNewlines();
122
+ if (this.current().type === type) return this.advance();
123
+ const tok = this.current();
124
+ this._error(message || `Expected '${type}' but got '${tok.type}' (${JSON.stringify(tok.value)})`, tok);
125
+ return tok; // error-recovery: pretend we consumed it
126
+ }
127
+
128
+ // ── Error handling / recovery ─────────────────────────────────────────────
129
+ _error(message, tok) {
130
+ tok = tok || this.current();
131
+ const err = {
132
+ message,
133
+ line: tok.line,
134
+ col: tok.col,
135
+ filename: this.filename,
136
+ };
137
+ this.errors.push(err);
138
+ throw new ParseError(`[ThyScript Parser] ${this.filename}:${tok.line}:${tok.col} - ${message}`, tok, this.filename);
139
+ }
140
+
141
+ /**
142
+ * After catching a ParseError, skip tokens until we reach a safe
143
+ * statement-boundary token so parsing can continue.
144
+ */
145
+ _synchronize() {
146
+ this.advance();
147
+ // Statement-starting keywords that mark a new statement
148
+ const stmtStarters = new Set([
149
+ TokenType.KW_PRINT, TokenType.KW_CONST, TokenType.KW_TABLE,
150
+ TokenType.KW_PUBLIC, TokenType.KW_PRIVATE, TokenType.KW_PROTECTED,
151
+ TokenType.KW_CLASS, TokenType.KW_CLASS_HTML,
152
+ TokenType.KW_IF, TokenType.KW_WHILE, TokenType.KW_FOR,
153
+ TokenType.KW_RETURN, TokenType.KW_FUNC, TokenType.KW_ASYNC,
154
+ TokenType.KW_TRY, TokenType.KW_IMPORT, TokenType.KW_EXPORT,
155
+ TokenType.KW_SERVER, TokenType.KW_ROUTE, TokenType.KW_SOCKET,
156
+ TokenType.KW_DATABASE, TokenType.KW_COMPONENT,
157
+ TokenType.KW_NEURAL, TokenType.KW_PHYSICS, TokenType.KW_ANIMATE,
158
+ TokenType.KW_PARALLEL, TokenType.KW_WORKER, TokenType.KW_STREAM,
159
+ TokenType.KW_PIPELINE, TokenType.KW_MATCH,
160
+ TokenType.KW_ADD, TokenType.KW_CHANGE, TokenType.KW_CALCULATE,
161
+ TokenType.KW_SAVE, TokenType.KW_FREE, TokenType.KW_BREAK,
162
+ TokenType.KW_CONTINUE, TokenType.KW_THROW,
163
+ TokenType.KW_END, TokenType.EOF,
164
+ ]);
165
+
166
+ while (!this.check(TokenType.EOF)) {
167
+ if (this.check(TokenType.NEWLINE)) { this.advance(); break; }
168
+ if (stmtStarters.has(this.current().type)) break;
169
+ this.advance();
170
+ }
171
+ }
172
+
173
+ // ── Entry point ───────────────────────────────────────────────────────────
174
+ /**
175
+ * Parse the source text and return the root Program AST node.
176
+ * @returns {{ ast: Program, errors: Array }}
177
+ */
178
+ parse() {
179
+ this._lex();
180
+ const line = this.current().line;
181
+ const col = this.current().col;
182
+ const body = this._parseStatementList(/*stopAtEnd*/ false);
183
+ this.expect(TokenType.EOF);
184
+ const ast = AST.Program(body, line, col);
185
+ return { ast, errors: this.errors };
186
+ }
187
+
188
+ // ── Statement list ────────────────────────────────────────────────────────
189
+ /**
190
+ * Parses a list of statements.
191
+ * @param {boolean} stopAtEnd – when true, stop at `end()` / `else` / `elif` / `catch` / `finally` / `when` / `otherwise` / EOF
192
+ */
193
+ _parseStatementList(stopAtEnd = true) {
194
+ const stmts = [];
195
+ while (true) {
196
+ this.skipNewlines();
197
+ const cur = this.current();
198
+
199
+ if (cur.type === TokenType.EOF) break;
200
+
201
+ if (stopAtEnd) {
202
+ if (cur.type === TokenType.KW_END) break;
203
+ if (cur.type === TokenType.KW_ELSE) break;
204
+ if (cur.type === TokenType.KW_ELIF) break;
205
+ if (cur.type === TokenType.KW_CATCH) break;
206
+ if (cur.type === TokenType.KW_FINALLY) break;
207
+ if (cur.type === TokenType.KW_WHEN) break;
208
+ if (cur.type === TokenType.KW_OTHERWISE) break;
209
+ }
210
+
211
+ try {
212
+ const stmt = this._parseStatement();
213
+ if (stmt) stmts.push(stmt);
214
+ } catch (e) {
215
+ if (e instanceof ParseError) {
216
+ this._synchronize();
217
+ } else {
218
+ throw e;
219
+ }
220
+ }
221
+ }
222
+ return stmts;
223
+ }
224
+
225
+ /** Parse and return a Block node (list wrapped in a Block) */
226
+ _parseBlock() {
227
+ const tok = this.current();
228
+ const stmts = this._parseStatementList(true);
229
+ return AST.Block(stmts, tok.line, tok.col);
230
+ }
231
+
232
+ /** Expect a colon then parse a block up to end() */
233
+ _expectColonBlock() {
234
+ this.skipNewlines();
235
+ this.expect(TokenType.COLON, "Expected ':' to open block");
236
+ this.skipNewlines();
237
+ return this._parseBlock();
238
+ }
239
+
240
+ // ── Statement dispatch ────────────────────────────────────────────────────
241
+ _parseStatement() {
242
+ this.skipNewlines();
243
+ const tok = this.current();
244
+
245
+ switch (tok.type) {
246
+ // ── declarations ──────────────────────────────────────────────────────
247
+ case TokenType.KW_CONST: return this._parseConstTable();
248
+ case TokenType.KW_TABLE: return this._parseConstTable(false);
249
+ case TokenType.KW_LET:
250
+ case TokenType.KW_VAR: return this._parseLetVar();
251
+ case TokenType.KW_EXPORT: return this._parseExport();
252
+ case TokenType.KW_IMPORT: return this._parseImport();
253
+
254
+ // ── function / class ──────────────────────────────────────────────────
255
+ case TokenType.KW_FUNC: return this._parseFuncDecl(false, false);
256
+ case TokenType.KW_ASYNC: return this._parseAsync();
257
+ case TokenType.KW_PUBLIC:
258
+ case TokenType.KW_PRIVATE:
259
+ case TokenType.KW_PROTECTED: return this._parseClassDecl();
260
+ case TokenType.KW_CLASS: return this._parseClassDecl();
261
+ case TokenType.KW_CLASS_HTML: return this._parseHtmlClassDecl();
262
+
263
+ // ── control flow ──────────────────────────────────────────────────────
264
+ case TokenType.KW_IF: return this._parseIf();
265
+ case TokenType.KW_WHILE: return this._parseWhile();
266
+ case TokenType.KW_FOR: return this._parseFor();
267
+ case TokenType.KW_RETURN: return this._parseReturn();
268
+ case TokenType.KW_BREAK: this.advance(); return AST.BreakStmt(tok.line, tok.col);
269
+ case TokenType.KW_CONTINUE: this.advance(); return AST.ContinueStmt(tok.line, tok.col);
270
+ case TokenType.KW_THROW: return this._parseThrow();
271
+
272
+ // ── ThyScript specials ────────────────────────────────────────────────
273
+ case TokenType.KW_PRINT: return this._parsePrint();
274
+ case TokenType.KW_ADD: return this._parseAdd();
275
+ case TokenType.KW_CHANGE: return this._parseChange();
276
+ case TokenType.KW_CALCULATE: return this._parseCalculateStmt();
277
+ case TokenType.KW_INPUT: return this._parseInputStmt();
278
+ case TokenType.KW_SAVE: return this._parseSave();
279
+ case TokenType.KW_FREE: return this._parseFree();
280
+
281
+ // ── exception handling ────────────────────────────────────────────────
282
+ case TokenType.KW_TRY: return this._parseTryCatch();
283
+
284
+ // ── server / networking ───────────────────────────────────────────────
285
+ case TokenType.KW_SERVER: return this._parseServer();
286
+ case TokenType.KW_ROUTE: return this._parseRoute();
287
+ case TokenType.KW_SOCKET: return this._parseSocket();
288
+
289
+ // ── database ──────────────────────────────────────────────────────────
290
+ case TokenType.KW_DATABASE: return this._parseDatabase();
291
+
292
+ // ── UI / components ───────────────────────────────────────────────────
293
+ case TokenType.KW_COMPONENT: return this._parseComponent();
294
+
295
+ // ── ML ────────────────────────────────────────────────────────────────
296
+ case TokenType.KW_NEURAL: return this._parseNeural();
297
+
298
+ // ── physics ───────────────────────────────────────────────────────────
299
+ case TokenType.KW_PHYSICS: return this._parsePhysics();
300
+
301
+ // ── animation ─────────────────────────────────────────────────────────
302
+ case TokenType.KW_ANIMATE: return this._parseAnimate();
303
+
304
+ // ── concurrency ───────────────────────────────────────────────────────
305
+ case TokenType.KW_PARALLEL: return this._parseParallel();
306
+ case TokenType.KW_WORKER: return this._parseWorker();
307
+ case TokenType.KW_STREAM: return this._parseStream();
308
+ case TokenType.KW_PIPELINE: return this._parsePipeline();
309
+
310
+ // ── pattern matching ──────────────────────────────────────────────────
311
+ case TokenType.KW_MATCH: return this._parseMatch();
312
+
313
+ // ── expression statement ──────────────────────────────────────────────
314
+ default:
315
+ return this._parseExpressionStatement();
316
+ }
317
+ }
318
+
319
+ // ═══════════════════════════════════════════════════════════════════════════
320
+ // Specific statement parsers
321
+ // ═══════════════════════════════════════════════════════════════════════════
322
+
323
+ // ── const table / table ───────────────────────────────────────────────────
324
+ /**
325
+ * `const table name = expr`
326
+ * `const table [a,b] = arr` (destructure)
327
+ * `const table {x,y} = obj` (destructure)
328
+ */
329
+ _parseConstTable(isConst = true) {
330
+ const tok = this.current();
331
+ this.advance(); // consume `const`
332
+
333
+ // If `const` was parsed but not `table`, allow `const table` as pair
334
+ if (isConst && this.check(TokenType.KW_TABLE)) {
335
+ this.advance(); // consume `table`
336
+ }
337
+ // else: just `table name = …` (mutable)
338
+
339
+ this.skipNewlines();
340
+ return this._parseVarDeclarator(isConst, tok);
341
+ }
342
+
343
+ _parseLetVar() {
344
+ const tok = this.current();
345
+ this.advance(); // consume let / var
346
+ return this._parseVarDeclarator(false, tok);
347
+ }
348
+
349
+ _parseVarDeclarator(isConst, tok) {
350
+ this.skipNewlines();
351
+ const cur = this.current();
352
+
353
+ // Destructure: const table [a, b] = arr
354
+ if (cur.type === TokenType.LBRACKET) {
355
+ return this._parseDestructureDecl('array', isConst, tok);
356
+ }
357
+ // Destructure: const table { x, y } = obj
358
+ if (cur.type === TokenType.LBRACE) {
359
+ return this._parseDestructureDecl('object', isConst, tok);
360
+ }
361
+
362
+ // Simple: name = expr
363
+ const name = this._expectIdentifierName('Expected variable name');
364
+ this.skipNewlines();
365
+
366
+ let init = null;
367
+ if (this.match(TokenType.ASSIGN)) {
368
+ this.skipNewlines();
369
+ init = this._parseExpression();
370
+ }
371
+
372
+ return AST.VarDecl(name, init, isConst, tok.line, tok.col);
373
+ }
374
+
375
+ _parseDestructureDecl(kind, isConst, tok) {
376
+ const targets = [];
377
+ if (kind === 'array') {
378
+ this.advance(); // consume [
379
+ while (!this.check(TokenType.RBRACKET) && !this.check(TokenType.EOF)) {
380
+ this.skipNewlines();
381
+ if (this.check(TokenType.SPREAD)) {
382
+ this.advance();
383
+ targets.push({ rest: true, name: this._expectIdentifierName('Expected rest element name') });
384
+ } else {
385
+ targets.push({ name: this._expectIdentifierName('Expected element name') });
386
+ }
387
+ this.skipNewlines();
388
+ if (!this.match(TokenType.COMMA)) break;
389
+ }
390
+ this.skipNewlines();
391
+ this.expect(TokenType.RBRACKET, "Expected ']' to close destructure");
392
+ } else {
393
+ this.advance(); // consume {
394
+ while (!this.check(TokenType.RBRACE) && !this.check(TokenType.EOF)) {
395
+ this.skipNewlines();
396
+ const key = this._expectIdentifierName('Expected property key');
397
+ let alias = key;
398
+ if (this.match(TokenType.COLON)) {
399
+ alias = this._expectIdentifierName('Expected alias name');
400
+ }
401
+ let defaultVal = null;
402
+ if (this.match(TokenType.ASSIGN)) {
403
+ defaultVal = this._parseExpression();
404
+ }
405
+ targets.push({ key, alias, default: defaultVal });
406
+ this.skipNewlines();
407
+ if (!this.match(TokenType.COMMA)) break;
408
+ }
409
+ this.skipNewlines();
410
+ this.expect(TokenType.RBRACE, "Expected '}' to close destructure");
411
+ }
412
+
413
+ this.skipNewlines();
414
+ this.expect(TokenType.ASSIGN, "Expected '=' in destructuring declaration");
415
+ this.skipNewlines();
416
+ const init = this._parseExpression();
417
+ return AST.DestructureDecl(kind, targets, init, isConst, tok.line, tok.col);
418
+ }
419
+
420
+ // ── func ──────────────────────────────────────────────────────────────────
421
+ /**
422
+ * `[async] func name(params): body end()`
423
+ */
424
+ _parseFuncDecl(isAsync, isExported) {
425
+ const tok = this.current();
426
+ this.advance(); // consume `func`
427
+ this.skipNewlines();
428
+ const name = this._expectIdentifierName('Expected function name');
429
+ this.skipNewlines();
430
+ const params = this._parseParamList();
431
+ const body = this._expectColonBlock();
432
+ this._expectEnd();
433
+ return AST.FunctionDecl(name, params, body, isAsync, isExported, tok.line, tok.col);
434
+ }
435
+
436
+ _parseAsync() {
437
+ const tok = this.current();
438
+ this.advance(); // consume `async`
439
+ this.skipNewlines();
440
+ if (this.check(TokenType.KW_FUNC)) {
441
+ return this._parseFuncDecl(true, false);
442
+ }
443
+ this._error('Expected `func` after `async`', tok);
444
+ }
445
+
446
+ /**
447
+ * Parse (param1, param2, ...rest)
448
+ * @returns {string[]}
449
+ */
450
+ _parseParamList() {
451
+ const params = [];
452
+ this.expect(TokenType.LPAREN, "Expected '(' for parameter list");
453
+ this.skipNewlines();
454
+ while (!this.check(TokenType.RPAREN) && !this.check(TokenType.EOF)) {
455
+ if (this.check(TokenType.SPREAD)) {
456
+ this.advance();
457
+ params.push('...' + this._expectIdentifierName('Expected rest param name'));
458
+ } else {
459
+ const name = this._expectIdentifierName('Expected parameter name');
460
+ let defaultVal = null;
461
+ if (this.match(TokenType.ASSIGN)) {
462
+ defaultVal = this._parseExpression();
463
+ }
464
+ params.push(defaultVal !== null ? { name, default: defaultVal } : name);
465
+ }
466
+ this.skipNewlines();
467
+ if (!this.match(TokenType.COMMA)) break;
468
+ this.skipNewlines();
469
+ }
470
+ this.expect(TokenType.RPAREN, "Expected ')' after parameters");
471
+ return params;
472
+ }
473
+
474
+ // ── class ─────────────────────────────────────────────────────────────────
475
+ /**
476
+ * `[public|private|protected] class(Name) [extends Super]: body end()`
477
+ */
478
+ _parseClassDecl() {
479
+ const tok = this.current();
480
+ let access = 'public';
481
+
482
+ // optional access modifier
483
+ if (checkAny(tok, TokenType.KW_PUBLIC, TokenType.KW_PRIVATE, TokenType.KW_PROTECTED)) {
484
+ access = tok.value;
485
+ this.advance();
486
+ this.skipNewlines();
487
+ }
488
+
489
+ this.expect(TokenType.KW_CLASS, "Expected 'class'");
490
+ this.skipNewlines();
491
+ this.expect(TokenType.LPAREN, "Expected '(' after 'class'");
492
+ const name = this._expectIdentifierName('Expected class name');
493
+ this.expect(TokenType.RPAREN, "Expected ')' after class name");
494
+ this.skipNewlines();
495
+
496
+ let superClass = null;
497
+ if (this.match(TokenType.KW_EXTENDS)) {
498
+ this.skipNewlines();
499
+ superClass = this._expectIdentifierName('Expected super-class name');
500
+ this.skipNewlines();
501
+ }
502
+
503
+ const body = this._expectColonBlock();
504
+ this._expectEnd();
505
+ return AST.ClassDecl(name, superClass, body, access, tok.line, tok.col);
506
+ }
507
+
508
+ // ── classHtml ─────────────────────────────────────────────────────────────
509
+ /**
510
+ * `classHtml(Name): rawHtml end()`
511
+ * The Lexer has already consumed the raw HTML content into a STRING token.
512
+ */
513
+ _parseHtmlClassDecl() {
514
+ const tok = this.current();
515
+ this.advance(); // consume `classHtml`
516
+ this.skipNewlines();
517
+ this.expect(TokenType.LPAREN, "Expected '(' after 'classHtml'");
518
+ const name = this._expectIdentifierName('Expected HTML class name');
519
+ this.expect(TokenType.RPAREN, "Expected ')'");
520
+ this.skipNewlines();
521
+ this.expect(TokenType.COLON, "Expected ':' after classHtml declaration");
522
+
523
+ // If the lexer provided a single raw HTML STRING token:
524
+ this.skipNewlines();
525
+ if (this.check(TokenType.STRING)) {
526
+ const html = this.current().value;
527
+ this.advance();
528
+ this.skipNewlines();
529
+ this._expectEnd();
530
+ return AST.HtmlClassDecl(name, html, tok.line, tok.col);
531
+ }
532
+
533
+ // Collect raw HTML lines until `end()`
534
+ const htmlLines = [];
535
+ while (!this.check(TokenType.EOF)) {
536
+ this.skipNewlines();
537
+ // Check for end()
538
+ if (this.check(TokenType.KW_END)) {
539
+ this._expectEnd();
540
+ break;
541
+ }
542
+ // Accept STRING tokens (raw HTML captured by lexer) or identifiers
543
+ const t = this.current();
544
+ htmlLines.push(t.value !== null && t.value !== undefined ? String(t.value) : t.type);
545
+ this.advance();
546
+ }
547
+
548
+ return AST.HtmlClassDecl(name, htmlLines.join('\n'), tok.line, tok.col);
549
+ }
550
+
551
+ // ── if / elif / else ──────────────────────────────────────────────────────
552
+ _parseIf() {
553
+ const tok = this.current();
554
+ this.advance(); // consume `if`
555
+ this.skipNewlines();
556
+ this.expect(TokenType.LPAREN, "Expected '(' after 'if'");
557
+ const test = this._parseExpression();
558
+ this.expect(TokenType.RPAREN, "Expected ')' after if-condition");
559
+ const consequent = this._expectColonBlock();
560
+
561
+ const elseifs = [];
562
+ let alternate = null;
563
+
564
+ while (true) {
565
+ this.skipNewlines();
566
+ if (this.check(TokenType.KW_ELIF)) {
567
+ this.advance();
568
+ this.skipNewlines();
569
+ this.expect(TokenType.LPAREN, "Expected '(' after 'elif'");
570
+ const elifTest = this._parseExpression();
571
+ this.expect(TokenType.RPAREN, "Expected ')'");
572
+ const elifBody = this._expectColonBlock();
573
+ elseifs.push({ test: elifTest, body: elifBody });
574
+ } else if (this.check(TokenType.KW_ELSE)) {
575
+ this.advance();
576
+ alternate = this._expectColonBlock();
577
+ break;
578
+ } else {
579
+ break;
580
+ }
581
+ }
582
+
583
+ this._expectEnd();
584
+ return AST.IfStmt(test, consequent, elseifs, alternate, tok.line, tok.col);
585
+ }
586
+
587
+ // ── while ─────────────────────────────────────────────────────────────────
588
+ _parseWhile() {
589
+ const tok = this.current();
590
+ this.advance();
591
+ this.skipNewlines();
592
+ this.expect(TokenType.LPAREN, "Expected '(' after 'while'");
593
+ const test = this._parseExpression();
594
+ this.expect(TokenType.RPAREN, "Expected ')'");
595
+ const body = this._expectColonBlock();
596
+ this._expectEnd();
597
+ return AST.WhileStmt(test, body, tok.line, tok.col);
598
+ }
599
+
600
+ // ── for x in y ────────────────────────────────────────────────────────────
601
+ _parseFor() {
602
+ const tok = this.current();
603
+ this.advance();
604
+ this.skipNewlines();
605
+ const variable = this._expectIdentifierName('Expected loop variable');
606
+ this.skipNewlines();
607
+ this.expect(TokenType.KW_IN, "Expected 'in'");
608
+ this.skipNewlines();
609
+ const iterable = this._parseExpression();
610
+ const body = this._expectColonBlock();
611
+ this._expectEnd();
612
+ return AST.ForStmt(variable, iterable, body, tok.line, tok.col);
613
+ }
614
+
615
+ // ── return ────────────────────────────────────────────────────────────────
616
+ _parseReturn() {
617
+ const tok = this.current();
618
+ this.advance();
619
+ let argument = null;
620
+ // If the next non-whitespace thing is not a newline/EOF/end, parse the value
621
+ const next = this.current();
622
+ if (next.type !== TokenType.NEWLINE && next.type !== TokenType.EOF && next.type !== TokenType.KW_END) {
623
+ argument = this._parseExpression();
624
+ }
625
+ return AST.ReturnStmt(argument, tok.line, tok.col);
626
+ }
627
+
628
+ // ── throw ─────────────────────────────────────────────────────────────────
629
+ _parseThrow() {
630
+ const tok = this.current();
631
+ this.advance();
632
+ const argument = this._parseExpression();
633
+ return AST.ThrowStmt(argument, tok.line, tok.col);
634
+ }
635
+
636
+ // ── print ─────────────────────────────────────────────────────────────────
637
+ _parsePrint() {
638
+ const tok = this.current();
639
+ this.advance();
640
+ this.skipNewlines();
641
+ this.expect(TokenType.LPAREN, "Expected '(' after 'print'");
642
+ const args = [];
643
+ while (!this.check(TokenType.RPAREN) && !this.check(TokenType.EOF)) {
644
+ args.push(this._parseExpression());
645
+ this.skipNewlines();
646
+ if (!this.match(TokenType.COMMA)) break;
647
+ this.skipNewlines();
648
+ }
649
+ this.expect(TokenType.RPAREN, "Expected ')' after print arguments");
650
+ return AST.PrintStmt(args, tok.line, tok.col);
651
+ }
652
+
653
+ // ── add N in list ─────────────────────────────────────────────────────────
654
+ _parseAdd() {
655
+ const tok = this.current();
656
+ this.advance(); // consume `add`
657
+ this.skipNewlines();
658
+ const value = this._parseExpression();
659
+ this.skipNewlines();
660
+ this.expect(TokenType.KW_IN, "Expected 'in' after value in 'add'");
661
+ this.skipNewlines();
662
+ let target = this._expectIdentifierName('Expected list name');
663
+ while (this.check(TokenType.DOT)) {
664
+ this.advance();
665
+ target += '.' + this._expectIdentifierName('Expected property name');
666
+ }
667
+ return AST.AddStmt(value, target, tok.line, tok.col);
668
+ }
669
+
670
+ // ── change(x to(y)) ───────────────────────────────────────────────────────
671
+ _parseChange() {
672
+ const tok = this.current();
673
+ this.advance(); // consume `change`
674
+ this.skipNewlines();
675
+ this.expect(TokenType.LPAREN, "Expected '(' after 'change'");
676
+ this.skipNewlines();
677
+ let target = this._expectIdentifierName('Expected variable name');
678
+ while (this.check(TokenType.DOT)) {
679
+ this.advance();
680
+ target += '.' + this._expectIdentifierName('Expected property name');
681
+ }
682
+ this.skipNewlines();
683
+ this.expect(TokenType.KW_TO, "Expected 'to'");
684
+ this.skipNewlines();
685
+ this.expect(TokenType.LPAREN, "Expected '(' after 'to'");
686
+ const value = this._parseExpression();
687
+ this.expect(TokenType.RPAREN, "Expected ')'");
688
+ this.expect(TokenType.RPAREN, "Expected ')' to close 'change'");
689
+ return AST.ChangeStmt(target, value, tok.line, tok.col);
690
+ }
691
+
692
+ // ── calculate(expr) ───────────────────────────────────────────────────────
693
+ _parseCalculateStmt() {
694
+ const tok = this.current();
695
+ this.advance();
696
+ this.skipNewlines();
697
+ this.expect(TokenType.LPAREN, "Expected '('");
698
+ const expr = this._parseExpression();
699
+ this.expect(TokenType.RPAREN, "Expected ')'");
700
+ return AST.CalculateStmt(expr, tok.line, tok.col);
701
+ }
702
+
703
+ // ── input(prompt) used as statement ──────────────────────────────────────
704
+ _parseInputStmt() {
705
+ const tok = this.current();
706
+ const expr = this._parseInputExpr();
707
+ return AST.ExprStmt(expr, tok.line, tok.col);
708
+ }
709
+
710
+ _parseInputExpr() {
711
+ const tok = this.current();
712
+ this.advance(); // consume `input`
713
+ this.skipNewlines();
714
+ this.expect(TokenType.LPAREN, "Expected '('");
715
+ let prompt = null;
716
+ if (!this.check(TokenType.RPAREN)) {
717
+ prompt = this._parseExpression();
718
+ }
719
+ this.expect(TokenType.RPAREN, "Expected ')'");
720
+ return AST.InputExpr(prompt, tok.line, tok.col);
721
+ }
722
+
723
+ // ── save(var) ─────────────────────────────────────────────────────────────
724
+ _parseSave() {
725
+ const tok = this.current();
726
+ this.advance();
727
+ this.skipNewlines();
728
+ this.expect(TokenType.LPAREN, "Expected '('");
729
+ const name = this._expectIdentifierName('Expected variable name');
730
+ this.expect(TokenType.RPAREN, "Expected ')'");
731
+ return AST.SaveStmt(name, tok.line, tok.col);
732
+ }
733
+
734
+ // ── free() ────────────────────────────────────────────────────────────────
735
+ _parseFree() {
736
+ const tok = this.current();
737
+ this.advance();
738
+ this.skipNewlines();
739
+ if (this.check(TokenType.LPAREN)) {
740
+ this.advance();
741
+ this.skipNewlines();
742
+ this.expect(TokenType.RPAREN, "Expected ')'");
743
+ }
744
+ return AST.ExprStmt(AST.FreeExpr(tok.line, tok.col), tok.line, tok.col);
745
+ }
746
+
747
+ // ── try/catch/finally ─────────────────────────────────────────────────────
748
+ _parseTryCatch() {
749
+ const tok = this.current();
750
+ this.advance(); // consume `try`
751
+ const body = this._expectColonBlock();
752
+
753
+ this.skipNewlines();
754
+ let errorVar = null;
755
+ let handler = AST.Block([], tok.line, tok.col);
756
+ let finalizer = null;
757
+
758
+ if (this.check(TokenType.KW_CATCH)) {
759
+ this.advance();
760
+ this.skipNewlines();
761
+ if (this.check(TokenType.LPAREN)) {
762
+ this.advance();
763
+ errorVar = this._expectIdentifierName('Expected error variable name');
764
+ this.expect(TokenType.RPAREN, "Expected ')'");
765
+ }
766
+ handler = this._expectColonBlock();
767
+ this.skipNewlines();
768
+ }
769
+
770
+ if (this.check(TokenType.KW_FINALLY)) {
771
+ this.advance();
772
+ finalizer = this._expectColonBlock();
773
+ this.skipNewlines();
774
+ }
775
+
776
+ this._expectEnd();
777
+ return AST.TryCatch(body, errorVar, handler, finalizer, tok.line, tok.col);
778
+ }
779
+
780
+ // ── import name from 'module' ─────────────────────────────────────────────
781
+ _parseImport() {
782
+ const tok = this.current();
783
+ this.advance(); // consume `import`
784
+ this.skipNewlines();
785
+
786
+ const specifiers = [];
787
+ let isDefault = false;
788
+
789
+ // import { a, b } from 'mod'
790
+ if (this.check(TokenType.LBRACE)) {
791
+ this.advance();
792
+ while (!this.check(TokenType.RBRACE) && !this.check(TokenType.EOF)) {
793
+ this.skipNewlines();
794
+ specifiers.push(this._expectIdentifierName('Expected import name'));
795
+ this.skipNewlines();
796
+ if (!this.match(TokenType.COMMA)) break;
797
+ }
798
+ this.skipNewlines();
799
+ this.expect(TokenType.RBRACE, "Expected '}'");
800
+ } else if (this.check(TokenType.STAR)) {
801
+ // import * from 'mod'
802
+ this.advance();
803
+ specifiers.push('*');
804
+ } else {
805
+ // import name from 'mod' (default import)
806
+ isDefault = true;
807
+ specifiers.push(this._expectIdentifierName('Expected import name'));
808
+ }
809
+
810
+ this.skipNewlines();
811
+ this.expect(TokenType.KW_FROM, "Expected 'from'");
812
+ this.skipNewlines();
813
+ const source = this._expectString('Expected module path string');
814
+
815
+ return AST.ImportDecl(specifiers, source, isDefault, tok.line, tok.col);
816
+ }
817
+
818
+ // ── export ────────────────────────────────────────────────────────────────
819
+ _parseExport() {
820
+ const tok = this.current();
821
+ this.advance(); // consume `export`
822
+ this.skipNewlines();
823
+ let decl;
824
+ if (this.check(TokenType.KW_FUNC)) {
825
+ decl = this._parseFuncDecl(false, true);
826
+ } else if (this.check(TokenType.KW_ASYNC)) {
827
+ this.advance();
828
+ this.skipNewlines();
829
+ decl = this._parseFuncDecl(true, true);
830
+ } else if (this.check(TokenType.KW_CONST)) {
831
+ decl = this._parseConstTable(true);
832
+ } else if (this.check(TokenType.KW_TABLE)) {
833
+ decl = this._parseConstTable(false);
834
+ } else {
835
+ decl = this._parseStatement();
836
+ }
837
+ return AST.ExportDecl(decl, tok.line, tok.col);
838
+ }
839
+
840
+ // ── server ────────────────────────────────────────────────────────────────
841
+ _parseServer() {
842
+ const tok = this.current();
843
+ this.advance();
844
+ this.skipNewlines();
845
+ let port = null;
846
+ if (this.check(TokenType.LPAREN)) {
847
+ this.advance();
848
+ if (!this.check(TokenType.RPAREN)) {
849
+ port = this._parseExpression();
850
+ }
851
+ this.expect(TokenType.RPAREN, "Expected ')'");
852
+ this.skipNewlines();
853
+ }
854
+ const body = this._expectColonBlock();
855
+ this._expectEnd();
856
+ return AST.ServerDecl(body, port, tok.line, tok.col);
857
+ }
858
+
859
+ // ── route METHOD '/path' ──────────────────────────────────────────────────
860
+ _parseRoute() {
861
+ const tok = this.current();
862
+ this.advance(); // consume `route`
863
+ this.skipNewlines();
864
+
865
+ let method = 'get';
866
+ if (checkAny(this.current(),
867
+ TokenType.KW_GET, TokenType.KW_POST, TokenType.KW_PUT,
868
+ TokenType.KW_DELETE_HTTP, TokenType.KW_PATCH)) {
869
+ method = this.current().value.toLowerCase();
870
+ this.advance();
871
+ }
872
+ this.skipNewlines();
873
+ const path = this._parseExpression();
874
+ const body = this._expectColonBlock();
875
+ this._expectEnd();
876
+ return AST.RouteDecl(method, path, body, tok.line, tok.col);
877
+ }
878
+
879
+ // ── socket on 'event' ─────────────────────────────────────────────────────
880
+ _parseSocket() {
881
+ const tok = this.current();
882
+ this.advance(); // consume `socket`
883
+ this.skipNewlines();
884
+ this.expect(TokenType.KW_ON, "Expected 'on' after 'socket'");
885
+ this.skipNewlines();
886
+ const event = this._parseExpression();
887
+ const body = this._expectColonBlock();
888
+ this._expectEnd();
889
+ return AST.SocketDecl(event, body, tok.line, tok.col);
890
+ }
891
+
892
+ // ── database connect ──────────────────────────────────────────────────────
893
+ _parseDatabase() {
894
+ const tok = this.current();
895
+ this.advance();
896
+ this.skipNewlines();
897
+ let connectionExpr = null;
898
+ if (this.match(TokenType.KW_CONNECT)) {
899
+ this.skipNewlines();
900
+ if (this.check(TokenType.LPAREN)) {
901
+ this.advance();
902
+ connectionExpr = this._parseExpression();
903
+ this.expect(TokenType.RPAREN, "Expected ')'");
904
+ this.skipNewlines();
905
+ }
906
+ }
907
+ const body = this._expectColonBlock();
908
+ this._expectEnd();
909
+ return AST.DatabaseDecl(body, connectionExpr, tok.line, tok.col);
910
+ }
911
+
912
+ // ── component Name ────────────────────────────────────────────────────────
913
+ _parseComponent() {
914
+ const tok = this.current();
915
+ this.advance();
916
+ this.skipNewlines();
917
+ const name = this._expectIdentifierName('Expected component name');
918
+ const body = this._expectColonBlock();
919
+ this._expectEnd();
920
+ return AST.ComponentDecl(name, body, tok.line, tok.col);
921
+ }
922
+
923
+ // ── neural ────────────────────────────────────────────────────────────────
924
+ _parseNeural() {
925
+ const tok = this.current();
926
+ this.advance();
927
+ this.skipNewlines();
928
+ let config = null;
929
+ if (this.check(TokenType.LPAREN)) {
930
+ this.advance();
931
+ config = this._parseExpression();
932
+ this.expect(TokenType.RPAREN, "Expected ')'");
933
+ this.skipNewlines();
934
+ }
935
+ const body = this._expectColonBlock();
936
+ this._expectEnd();
937
+ return AST.NeuralDecl(body, config, tok.line, tok.col);
938
+ }
939
+
940
+ // ── physics ───────────────────────────────────────────────────────────────
941
+ _parsePhysics() {
942
+ const tok = this.current();
943
+ this.advance();
944
+ this.skipNewlines();
945
+ let config = null;
946
+ if (this.check(TokenType.LPAREN)) {
947
+ this.advance();
948
+ config = this._parseExpression();
949
+ this.expect(TokenType.RPAREN, "Expected ')'");
950
+ this.skipNewlines();
951
+ }
952
+ const body = this._expectColonBlock();
953
+ this._expectEnd();
954
+ return AST.PhysicsDecl(body, config, tok.line, tok.col);
955
+ }
956
+
957
+ // ── animate ───────────────────────────────────────────────────────────────
958
+ _parseAnimate() {
959
+ const tok = this.current();
960
+ this.advance();
961
+ this.skipNewlines();
962
+ let target = null;
963
+ if (this.check(TokenType.LPAREN)) {
964
+ this.advance();
965
+ target = this._parseExpression();
966
+ this.expect(TokenType.RPAREN, "Expected ')'");
967
+ this.skipNewlines();
968
+ }
969
+ const body = this._expectColonBlock();
970
+ this._expectEnd();
971
+ return AST.AnimateDecl(body, target, tok.line, tok.col);
972
+ }
973
+
974
+ // ── parallel ──────────────────────────────────────────────────────────────
975
+ _parseParallel() {
976
+ const tok = this.current();
977
+ this.advance();
978
+ const body = this._expectColonBlock();
979
+ this._expectEnd();
980
+ return AST.ParallelDecl(body, tok.line, tok.col);
981
+ }
982
+
983
+ // ── worker ────────────────────────────────────────────────────────────────
984
+ _parseWorker() {
985
+ const tok = this.current();
986
+ this.advance();
987
+ this.skipNewlines();
988
+ let name = null;
989
+ if (this.check(TokenType.IDENTIFIER)) {
990
+ name = this.advance().value;
991
+ }
992
+ const body = this._expectColonBlock();
993
+ this._expectEnd();
994
+ return AST.WorkerDecl(name, body, tok.line, tok.col);
995
+ }
996
+
997
+ // ── stream ────────────────────────────────────────────────────────────────
998
+ _parseStream() {
999
+ const tok = this.current();
1000
+ this.advance();
1001
+ this.skipNewlines();
1002
+ let name = null;
1003
+ if (this.check(TokenType.IDENTIFIER)) {
1004
+ name = this.advance().value;
1005
+ }
1006
+ const body = this._expectColonBlock();
1007
+ this._expectEnd();
1008
+ return AST.StreamDecl(name, body, tok.line, tok.col);
1009
+ }
1010
+
1011
+ // ── pipeline ──────────────────────────────────────────────────────────────
1012
+ _parsePipeline() {
1013
+ const tok = this.current();
1014
+ this.advance();
1015
+ this.skipNewlines();
1016
+ let name = null;
1017
+ if (this.check(TokenType.IDENTIFIER)) {
1018
+ name = this.advance().value;
1019
+ }
1020
+ const body = this._expectColonBlock();
1021
+ this._expectEnd();
1022
+ return AST.PipelineDecl(name, body, tok.line, tok.col);
1023
+ }
1024
+
1025
+ // ── match x: when y:…end() ───────────────────────────────────────────────
1026
+ _parseMatch() {
1027
+ const tok = this.current();
1028
+ this.advance(); // consume `match`
1029
+ this.skipNewlines();
1030
+ const subject = this._parseExpression();
1031
+ this.skipNewlines();
1032
+ this.expect(TokenType.COLON, "Expected ':' after match subject");
1033
+
1034
+ const arms = [];
1035
+ let otherwise = null;
1036
+
1037
+ while (true) {
1038
+ this.skipNewlines();
1039
+ if (this.check(TokenType.KW_WHEN)) {
1040
+ this.advance();
1041
+ this.skipNewlines();
1042
+ const pattern = this._parseExpression();
1043
+ const armBody = this._expectColonBlock();
1044
+ arms.push({ pattern, body: armBody });
1045
+ } else if (this.check(TokenType.KW_OTHERWISE)) {
1046
+ this.advance();
1047
+ otherwise = this._expectColonBlock();
1048
+ break;
1049
+ } else {
1050
+ break;
1051
+ }
1052
+ }
1053
+
1054
+ this._expectEnd();
1055
+ return AST.MatchExpr(subject, arms, otherwise, tok.line, tok.col);
1056
+ }
1057
+
1058
+ // ── expression statement ──────────────────────────────────────────────────
1059
+ _parseExpressionStatement() {
1060
+ const tok = this.current();
1061
+ const expr = this._parseExpression();
1062
+ return AST.ExprStmt(expr, tok.line, tok.col);
1063
+ }
1064
+
1065
+ // ═══════════════════════════════════════════════════════════════════════════
1066
+ // Expression Parsing – Pratt-style recursive descent
1067
+ // ═══════════════════════════════════════════════════════════════════════════
1068
+
1069
+ _parseExpression() {
1070
+ return this._parseAssignment();
1071
+ }
1072
+
1073
+ // ── Assignment (=, +=, -=, *=, /=, %=) ──────────────────────────────────
1074
+ _parseAssignment() {
1075
+ const left = this._parseTernary();
1076
+
1077
+ const assignOps = [
1078
+ TokenType.ASSIGN,
1079
+ TokenType.PLUS_ASSIGN,
1080
+ TokenType.MINUS_ASSIGN,
1081
+ TokenType.MUL_ASSIGN,
1082
+ TokenType.DIV_ASSIGN,
1083
+ TokenType.MOD_ASSIGN,
1084
+ ];
1085
+
1086
+ const tok = this.current();
1087
+ if (assignOps.includes(tok.type)) {
1088
+ this.advance();
1089
+ this.skipNewlines();
1090
+ const right = this._parseAssignment(); // right-associative
1091
+ return AST.AssignExpr(tok.value, left, right, tok.line, tok.col);
1092
+ }
1093
+
1094
+ return left;
1095
+ }
1096
+
1097
+ // ── Ternary ───────────────────────────────────────────────────────────────
1098
+ _parseTernary() {
1099
+ const expr = this._parseNullCoalesce();
1100
+ const tok = this.current();
1101
+ if (tok.type === TokenType.QUESTION) {
1102
+ this.advance();
1103
+ const consequent = this._parseExpression();
1104
+ this.expect(TokenType.COLON, "Expected ':' in ternary expression");
1105
+ const alternate = this._parseTernary();
1106
+ return AST.ConditionalExpr(expr, consequent, alternate, tok.line, tok.col);
1107
+ }
1108
+ return expr;
1109
+ }
1110
+
1111
+ // ── Null-coalesce ?? ─────────────────────────────────────────────────────
1112
+ _parseNullCoalesce() {
1113
+ let left = this._parseLogicalOr();
1114
+ while (this.current().type === TokenType.NULL_COALESCE) {
1115
+ const tok = this.advance();
1116
+ const right = this._parseLogicalOr();
1117
+ left = AST.BinaryExpr('??', left, right, tok.line, tok.col);
1118
+ }
1119
+ return left;
1120
+ }
1121
+
1122
+ // ── Logical OR || or ────────────────────────────────────────────────────
1123
+ _parseLogicalOr() {
1124
+ let left = this._parseLogicalAnd();
1125
+ while (checkAny(this.current(), TokenType.OR_OR, TokenType.KW_OR)) {
1126
+ const tok = this.advance();
1127
+ const right = this._parseLogicalAnd();
1128
+ left = AST.BinaryExpr('||', left, right, tok.line, tok.col);
1129
+ }
1130
+ return left;
1131
+ }
1132
+
1133
+ // ── Logical AND && and ──────────────────────────────────────────────────
1134
+ _parseLogicalAnd() {
1135
+ let left = this._parseBitwiseOr();
1136
+ while (checkAny(this.current(), TokenType.AND_AND, TokenType.KW_AND)) {
1137
+ const tok = this.advance();
1138
+ const right = this._parseBitwiseOr();
1139
+ left = AST.BinaryExpr('&&', left, right, tok.line, tok.col);
1140
+ }
1141
+ return left;
1142
+ }
1143
+
1144
+ // ── Bitwise OR | ─────────────────────────────────────────────────────────
1145
+ _parseBitwiseOr() {
1146
+ let left = this._parseBitwiseXor();
1147
+ while (this.current().type === TokenType.PIPE) {
1148
+ const tok = this.advance();
1149
+ const right = this._parseBitwiseXor();
1150
+ left = AST.BinaryExpr('|', left, right, tok.line, tok.col);
1151
+ }
1152
+ return left;
1153
+ }
1154
+
1155
+ // ── Bitwise XOR ^ ────────────────────────────────────────────────────────
1156
+ _parseBitwiseXor() {
1157
+ let left = this._parseBitwiseAnd();
1158
+ while (this.current().type === TokenType.CARET) {
1159
+ const tok = this.advance();
1160
+ const right = this._parseBitwiseAnd();
1161
+ left = AST.BinaryExpr('^', left, right, tok.line, tok.col);
1162
+ }
1163
+ return left;
1164
+ }
1165
+
1166
+ // ── Bitwise AND & ────────────────────────────────────────────────────────
1167
+ _parseBitwiseAnd() {
1168
+ let left = this._parseEquality();
1169
+ while (this.current().type === TokenType.AMPERSAND) {
1170
+ const tok = this.advance();
1171
+ const right = this._parseEquality();
1172
+ left = AST.BinaryExpr('&', left, right, tok.line, tok.col);
1173
+ }
1174
+ return left;
1175
+ }
1176
+
1177
+ // ── Equality == != === !== ────────────────────────────────────────────────
1178
+ _parseEquality() {
1179
+ let left = this._parseRelational();
1180
+ while (checkAny(this.current(),
1181
+ TokenType.EQ_EQ, TokenType.NOT_EQ,
1182
+ TokenType.EQ_STRICT, TokenType.NOT_EQ_STRICT)) {
1183
+ const tok = this.advance();
1184
+ const right = this._parseRelational();
1185
+ left = AST.BinaryExpr(tok.value, left, right, tok.line, tok.col);
1186
+ }
1187
+ return left;
1188
+ }
1189
+
1190
+ // ── Relational < <= > >= instanceof ─────────────────────────────────────
1191
+ _parseRelational() {
1192
+ let left = this._parseBitwiseShift();
1193
+ while (checkAny(this.current(),
1194
+ TokenType.LT, TokenType.LT_EQ,
1195
+ TokenType.GT, TokenType.GT_EQ,
1196
+ TokenType.KW_INSTANCEOF)) {
1197
+ const tok = this.advance();
1198
+ const right = this._parseBitwiseShift();
1199
+ left = AST.BinaryExpr(tok.value, left, right, tok.line, tok.col);
1200
+ }
1201
+ return left;
1202
+ }
1203
+
1204
+ // ── Bitwise shift << >> ──────────────────────────────────────────────────
1205
+ _parseBitwiseShift() {
1206
+ let left = this._parseAdditive();
1207
+ while (checkAny(this.current(), TokenType.SHIFT_LEFT, TokenType.SHIFT_RIGHT)) {
1208
+ const tok = this.advance();
1209
+ const right = this._parseAdditive();
1210
+ left = AST.BinaryExpr(tok.value, left, right, tok.line, tok.col);
1211
+ }
1212
+ return left;
1213
+ }
1214
+
1215
+ // ── Additive + - ─────────────────────────────────────────────────────────
1216
+ _parseAdditive() {
1217
+ let left = this._parseMultiplicative();
1218
+ while (checkAny(this.current(), TokenType.PLUS, TokenType.MINUS)) {
1219
+ const tok = this.advance();
1220
+ const right = this._parseMultiplicative();
1221
+ left = AST.BinaryExpr(tok.value, left, right, tok.line, tok.col);
1222
+ }
1223
+ return left;
1224
+ }
1225
+
1226
+ // ── Multiplicative * / % ─────────────────────────────────────────────────
1227
+ _parseMultiplicative() {
1228
+ let left = this._parseExponentiation();
1229
+ while (checkAny(this.current(), TokenType.STAR, TokenType.SLASH, TokenType.PERCENT)) {
1230
+ const tok = this.advance();
1231
+ const right = this._parseExponentiation();
1232
+ left = AST.BinaryExpr(tok.value, left, right, tok.line, tok.col);
1233
+ }
1234
+ return left;
1235
+ }
1236
+
1237
+ // ── Exponentiation ** (right-associative) ────────────────────────────────
1238
+ _parseExponentiation() {
1239
+ const base = this._parseUnaryPrefix();
1240
+ if (this.current().type === TokenType.POWER) {
1241
+ const tok = this.advance();
1242
+ const right = this._parseExponentiation(); // right-assoc
1243
+ return AST.BinaryExpr('**', base, right, tok.line, tok.col);
1244
+ }
1245
+ return base;
1246
+ }
1247
+
1248
+ // ── Unary prefix - ! ~ typeof not new await yield ────────────────────────
1249
+ _parseUnaryPrefix() {
1250
+ const tok = this.current();
1251
+
1252
+ switch (tok.type) {
1253
+ case TokenType.MINUS: {
1254
+ this.advance();
1255
+ const arg = this._parseUnaryPrefix();
1256
+ return AST.UnaryExpr('-', arg, true, tok.line, tok.col);
1257
+ }
1258
+ case TokenType.NOT:
1259
+ case TokenType.KW_NOT: {
1260
+ this.advance();
1261
+ const arg = this._parseUnaryPrefix();
1262
+ return AST.UnaryExpr('!', arg, true, tok.line, tok.col);
1263
+ }
1264
+ case TokenType.TILDE: {
1265
+ this.advance();
1266
+ const arg = this._parseUnaryPrefix();
1267
+ return AST.UnaryExpr('~', arg, true, tok.line, tok.col);
1268
+ }
1269
+ case TokenType.KW_TYPEOF: {
1270
+ this.advance();
1271
+ const arg = this._parseUnaryPrefix();
1272
+ return AST.UnaryExpr('typeof', arg, true, tok.line, tok.col);
1273
+ }
1274
+ case TokenType.KW_NEW: {
1275
+ this.advance();
1276
+ this.skipNewlines();
1277
+ const callee = this._parsePostfix(this._parsePrimary());
1278
+ const args = this.check(TokenType.LPAREN) ? this._parseArgList() : [];
1279
+ return AST.NewExpr(callee, args, tok.line, tok.col);
1280
+ }
1281
+ case TokenType.KW_AWAIT: {
1282
+ this.advance();
1283
+ const arg = this._parseUnaryPrefix();
1284
+ return AST.UnaryExpr('await', arg, true, tok.line, tok.col);
1285
+ }
1286
+ case TokenType.KW_YIELD: {
1287
+ this.advance();
1288
+ const arg = this._parseUnaryPrefix();
1289
+ return AST.UnaryExpr('yield', arg, true, tok.line, tok.col);
1290
+ }
1291
+ case TokenType.PLUS_PLUS: {
1292
+ this.advance();
1293
+ const arg = this._parseUnaryPrefix();
1294
+ return AST.UnaryExpr('++', arg, true, tok.line, tok.col);
1295
+ }
1296
+ case TokenType.MINUS_MINUS: {
1297
+ this.advance();
1298
+ const arg = this._parseUnaryPrefix();
1299
+ return AST.UnaryExpr('--', arg, true, tok.line, tok.col);
1300
+ }
1301
+ case TokenType.PLUS: {
1302
+ // unary + – mostly a no-op but valid
1303
+ this.advance();
1304
+ const arg = this._parseUnaryPrefix();
1305
+ return AST.UnaryExpr('+', arg, true, tok.line, tok.col);
1306
+ }
1307
+ default:
1308
+ return this._parsePostfix(this._parsePrimary());
1309
+ }
1310
+ }
1311
+
1312
+ // ── Postfix ++ -- ────────────────────────────────────────────────────────
1313
+ _parsePostfix(expr) {
1314
+ while (true) {
1315
+ const tok = this.current();
1316
+ if (tok.type === TokenType.PLUS_PLUS) {
1317
+ this.advance();
1318
+ expr = AST.UnaryExpr('++', expr, false, tok.line, tok.col);
1319
+ } else if (tok.type === TokenType.MINUS_MINUS) {
1320
+ this.advance();
1321
+ expr = AST.UnaryExpr('--', expr, false, tok.line, tok.col);
1322
+ } else if (tok.type === TokenType.LPAREN) {
1323
+ // call
1324
+ const args = this._parseArgList();
1325
+ expr = AST.CallExpr(expr, args, tok.line, tok.col);
1326
+ } else if (tok.type === TokenType.DOT) {
1327
+ this.advance();
1328
+ const prop = this._expectIdentifierName('Expected property name after \'.\'');
1329
+ expr = AST.MemberExpr(expr, prop, false, tok.line, tok.col);
1330
+ } else if (tok.type === TokenType.OPTIONAL_CHAIN) {
1331
+ this.advance();
1332
+ const prop = this._expectIdentifierName('Expected property name after \'?.\'');
1333
+ expr = AST.MemberExpr(expr, prop, true, tok.line, tok.col);
1334
+ } else if (tok.type === TokenType.LBRACKET) {
1335
+ this.advance();
1336
+ const index = this._parseExpression();
1337
+ this.expect(TokenType.RBRACKET, "Expected ']'");
1338
+ expr = AST.IndexExpr(expr, index, tok.line, tok.col);
1339
+ } else {
1340
+ break;
1341
+ }
1342
+ }
1343
+ return expr;
1344
+ }
1345
+
1346
+ /** Parse a comma-separated argument list inside ( ) */
1347
+ _parseArgList() {
1348
+ const tok = this.current();
1349
+ this.expect(TokenType.LPAREN, "Expected '('");
1350
+ const args = [];
1351
+ this.skipNewlines();
1352
+ while (!this.check(TokenType.RPAREN) && !this.check(TokenType.EOF)) {
1353
+ if (this.check(TokenType.SPREAD)) {
1354
+ const st = this.current();
1355
+ this.advance();
1356
+ args.push(AST.SpreadExpr(this._parseExpression(), st.line, st.col));
1357
+ } else {
1358
+ args.push(this._parseExpression());
1359
+ }
1360
+ this.skipNewlines();
1361
+ if (!this.match(TokenType.COMMA)) break;
1362
+ this.skipNewlines();
1363
+ }
1364
+ this.skipNewlines();
1365
+ this.expect(TokenType.RPAREN, "Expected ')' after argument list");
1366
+ return args;
1367
+ }
1368
+
1369
+ // ── Primary ───────────────────────────────────────────────────────────────
1370
+ _parsePrimary() {
1371
+ this.skipNewlines();
1372
+ const tok = this.current();
1373
+
1374
+ switch (tok.type) {
1375
+ // ── numeric literals ──────────────────────────────────────────────────
1376
+ case TokenType.NUMBER:
1377
+ case TokenType.FLOAT: {
1378
+ this.advance();
1379
+ return AST.Literal('number', tok.value, tok.line, tok.col);
1380
+ }
1381
+
1382
+ // ── string ────────────────────────────────────────────────────────────
1383
+ case TokenType.STRING: {
1384
+ this.advance();
1385
+ return AST.Literal('string', tok.value, tok.line, tok.col);
1386
+ }
1387
+
1388
+ // ── template literal ──────────────────────────────────────────────────
1389
+ case TokenType.TEMPLATE_LITERAL: {
1390
+ this.advance();
1391
+ // tok.value is an array of {type:'str'|'expr', value:string}
1392
+ // The expr parts are raw source strings – parse them into sub-ASTs.
1393
+ const parts = (tok.value || []).map(part => {
1394
+ if (part.type === 'str') return { type: 'str', value: part.value };
1395
+ // Parse the embedded expression source
1396
+ try {
1397
+ const subParser = new Parser(part.value, this.filename + ':template');
1398
+ const { ast: subAst } = subParser.parse();
1399
+ // subAst.body[0] is an ExprStmt wrapping the expression
1400
+ const exprNode = (subAst.body[0] && subAst.body[0].expression)
1401
+ ? subAst.body[0].expression
1402
+ : subAst.body[0];
1403
+ return { type: 'expr', value: exprNode };
1404
+ } catch (_) {
1405
+ return { type: 'expr', value: AST.Literal('string', part.value, tok.line, tok.col) };
1406
+ }
1407
+ });
1408
+ return AST.TemplateLiteral(parts, tok.line, tok.col);
1409
+ }
1410
+
1411
+ // ── booleans ──────────────────────────────────────────────────────────
1412
+ case TokenType.KW_TRUE:
1413
+ case TokenType.BOOLEAN: {
1414
+ this.advance();
1415
+ return AST.Literal('boolean', true, tok.line, tok.col);
1416
+ }
1417
+ case TokenType.KW_FALSE:
1418
+ case TokenType.KW_FAKE: {
1419
+ this.advance();
1420
+ return AST.Literal('boolean', false, tok.line, tok.col);
1421
+ }
1422
+
1423
+ // ── null / undefined ──────────────────────────────────────────────────
1424
+ case TokenType.NULL:
1425
+ case TokenType.KW_NULL: {
1426
+ this.advance();
1427
+ return AST.Literal('null', null, tok.line, tok.col);
1428
+ }
1429
+ case TokenType.KW_UNDEFINED: {
1430
+ this.advance();
1431
+ return AST.Literal('undefined', undefined, tok.line, tok.col);
1432
+ }
1433
+
1434
+ // ── this / self / super ───────────────────────────────────────────────
1435
+ case TokenType.KW_THIS:
1436
+ case TokenType.KW_SELF: {
1437
+ this.advance();
1438
+ return AST.Identifier('this', tok.line, tok.col);
1439
+ }
1440
+ case TokenType.KW_SUPER: {
1441
+ this.advance();
1442
+ return AST.Identifier('super', tok.line, tok.col);
1443
+ }
1444
+
1445
+ // ── grouped expression / arrow function ───────────────────────────────
1446
+ case TokenType.LPAREN: {
1447
+ return this._parseGroupedOrArrow();
1448
+ }
1449
+
1450
+ // ── array literal ─────────────────────────────────────────────────────
1451
+ case TokenType.LBRACKET: {
1452
+ return this._parseArrayLiteral();
1453
+ }
1454
+
1455
+ // ── object literal ────────────────────────────────────────────────────
1456
+ case TokenType.LBRACE: {
1457
+ return this._parseObjectLiteral();
1458
+ }
1459
+
1460
+ // ── input(…) used as expression ───────────────────────────────────────
1461
+ case TokenType.KW_INPUT: {
1462
+ return this._parseInputExpr();
1463
+ }
1464
+
1465
+ // ── calculate(…) as expression ────────────────────────────────────────
1466
+ case TokenType.KW_CALCULATE: {
1467
+ this.advance();
1468
+ this.expect(TokenType.LPAREN, "Expected '('");
1469
+ const expr = this._parseExpression();
1470
+ this.expect(TokenType.RPAREN, "Expected ')'");
1471
+ return AST.CalculateExpr(expr, tok.line, tok.col);
1472
+ }
1473
+
1474
+ // ── free() as expression ──────────────────────────────────────────────
1475
+ case TokenType.KW_FREE: {
1476
+ this.advance();
1477
+ if (this.check(TokenType.LPAREN)) {
1478
+ this.advance();
1479
+ this.skipNewlines();
1480
+ this.expect(TokenType.RPAREN, "Expected ')'");
1481
+ }
1482
+ return AST.FreeExpr(tok.line, tok.col);
1483
+ }
1484
+
1485
+ // ── spread used in an expression context ──────────────────────────────
1486
+ case TokenType.SPREAD: {
1487
+ this.advance();
1488
+ return AST.SpreadExpr(this._parseUnaryPrefix(), tok.line, tok.col);
1489
+ }
1490
+
1491
+ // ── identifier (and keyword-identifiers) ──────────────────────────────
1492
+ case TokenType.IDENTIFIER:
1493
+ return this._parseIdentifierPrimary();
1494
+
1495
+ // ── many ThyScript keywords are also valid identifiers when used as
1496
+ // a callee or member target (e.g. `print`, `save`, `load`, …)
1497
+ default:
1498
+ if (this._isKeywordUsableAsIdentifier(tok)) {
1499
+ this.advance();
1500
+ return AST.Identifier(tok.value, tok.line, tok.col);
1501
+ }
1502
+ this._error(`Unexpected token '${tok.type}' ('${tok.value}')`, tok);
1503
+ // fallback to keep parser in a sane state
1504
+ this.advance();
1505
+ return AST.Literal('null', null, tok.line, tok.col);
1506
+ }
1507
+ }
1508
+
1509
+ /**
1510
+ * True for keywords that can legitimately appear as property names /
1511
+ * function names / identifiers in expression position.
1512
+ */
1513
+ _isKeywordUsableAsIdentifier(tok) {
1514
+ const usable = new Set([
1515
+ TokenType.KW_PRINT, TokenType.KW_SAVE, TokenType.KW_FREE,
1516
+ TokenType.KW_INPUT, TokenType.KW_CALCULATE, TokenType.KW_ADD,
1517
+ TokenType.KW_CHANGE, TokenType.KW_TO, TokenType.KW_IN,
1518
+ TokenType.KW_GET, TokenType.KW_POST, TokenType.KW_PUT,
1519
+ TokenType.KW_PATCH, TokenType.KW_DELETE, TokenType.KW_DELETE_HTTP,
1520
+ TokenType.KW_CONNECT, TokenType.KW_LISTEN, TokenType.KW_ON,
1521
+ TokenType.KW_EMIT, TokenType.KW_SOCKET, TokenType.KW_ROUTE,
1522
+ TokenType.KW_SERVER, TokenType.KW_DATABASE, TokenType.KW_QUERY,
1523
+ TokenType.KW_INSERT, TokenType.KW_UPDATE, TokenType.KW_SELECT,
1524
+ TokenType.KW_WHERE, TokenType.KW_JOIN, TokenType.KW_CREATE,
1525
+ TokenType.KW_DROP, TokenType.KW_RENDER, TokenType.KW_COMPONENT,
1526
+ TokenType.KW_STATE, TokenType.KW_PROPS, TokenType.KW_STYLE,
1527
+ TokenType.KW_ANIMATE, TokenType.KW_NEURAL, TokenType.KW_PHYSICS,
1528
+ TokenType.KW_PARALLEL, TokenType.KW_WORKER, TokenType.KW_STREAM,
1529
+ TokenType.KW_PIPELINE, TokenType.KW_MATCH, TokenType.KW_WHEN,
1530
+ TokenType.KW_OTHERWISE, TokenType.KW_STATIC, TokenType.KW_ABSTRACT,
1531
+ TokenType.KW_INTERFACE, TokenType.KW_EXTENDS, TokenType.KW_IMPLEMENTS,
1532
+ TokenType.KW_LOAD, TokenType.KW_READ, TokenType.KW_WRITE,
1533
+ TokenType.KW_APPEND, TokenType.KW_EXISTS, TokenType.KW_MKDIR,
1534
+ TokenType.KW_PATH, TokenType.KW_ENV, TokenType.KW_CONFIG,
1535
+ TokenType.KW_PROCESS, TokenType.KW_EXIT, TokenType.KW_SLEEP,
1536
+ TokenType.KW_TIMEOUT, TokenType.KW_INTERVAL, TokenType.KW_FETCH,
1537
+ TokenType.KW_HTTP, TokenType.KW_HTTPS, TokenType.KW_URL,
1538
+ TokenType.KW_COOKIE, TokenType.KW_SESSION, TokenType.KW_HEADER,
1539
+ TokenType.KW_BODY, TokenType.KW_PARAM, TokenType.KW_FORM,
1540
+ TokenType.KW_UPLOAD, TokenType.KW_DOWNLOAD, TokenType.KW_SERVE,
1541
+ TokenType.KW_TEMPLATE, TokenType.KW_LAYOUT, TokenType.KW_PAGE,
1542
+ TokenType.KW_SECTION, TokenType.KW_WIDGET, TokenType.KW_BUTTON,
1543
+ TokenType.KW_CHART, TokenType.KW_GRAPH, TokenType.KW_PLOT,
1544
+ TokenType.KW_DASHBOARD, TokenType.KW_CARD, TokenType.KW_MODAL,
1545
+ TokenType.KW_TOAST, TokenType.KW_ALERT, TokenType.KW_NAV,
1546
+ TokenType.KW_MENU, TokenType.KW_SIDEBAR, TokenType.KW_FOOTER,
1547
+ TokenType.KW_THEME, TokenType.KW_DARK, TokenType.KW_LIGHT,
1548
+ TokenType.KW_GRID, TokenType.KW_FLEX, TokenType.KW_RESPONSIVE,
1549
+ TokenType.KW_MEDIA, TokenType.KW_TRANSITION, TokenType.KW_EFFECT,
1550
+ TokenType.KW_SHADOW, TokenType.KW_BORDER, TokenType.KW_RADIUS,
1551
+ TokenType.KW_PADDING, TokenType.KW_MARGIN, TokenType.KW_FONT,
1552
+ TokenType.KW_SIZE, TokenType.KW_WIDTH, TokenType.KW_HEIGHT,
1553
+ TokenType.KW_POSITION, TokenType.KW_LAYER, TokenType.KW_OPACITY,
1554
+ TokenType.KW_BLUR, TokenType.KW_CURSOR, TokenType.KW_HOVER,
1555
+ TokenType.KW_CLICK, TokenType.KW_SCROLL, TokenType.KW_DRAG,
1556
+ TokenType.KW_DROP, TokenType.KW_RESIZE, TokenType.KW_KEYBOARD,
1557
+ TokenType.KW_MOUSE, TokenType.KW_TOUCH, TokenType.KW_GAMEPAD,
1558
+ TokenType.KW_AUDIO, TokenType.KW_VIDEO, TokenType.KW_CAMERA,
1559
+ TokenType.KW_MICROPHONE,TokenType.KW_STORAGE, TokenType.KW_NOTIFICATION,
1560
+ TokenType.KW_PERMISSION,TokenType.KW_GEO, TokenType.KW_CLIPBOARD,
1561
+ TokenType.KW_NETWORK, TokenType.KW_WEBGL, TokenType.KW_WEBGPU,
1562
+ TokenType.KW_SHADER, TokenType.KW_VERTEX, TokenType.KW_FRAGMENT,
1563
+ TokenType.KW_TEXTURE, TokenType.KW_MESH, TokenType.KW_MODEL,
1564
+ TokenType.KW_MATERIAL, TokenType.KW_GEOMETRY, TokenType.KW_ROTATION,
1565
+ TokenType.KW_SCALE, TokenType.KW_TRANSLATE, TokenType.KW_VIEW,
1566
+ TokenType.KW_BLEND, TokenType.KW_DEPTH, TokenType.KW_UNIFORM,
1567
+ TokenType.KW_COMPUTE, TokenType.KW_DISPATCH, TokenType.KW_ATOMIC,
1568
+ TokenType.KW_SYNC, TokenType.KW_WAIT, TokenType.KW_NOTIFY,
1569
+ TokenType.KW_BROADCAST, TokenType.KW_CLUSTER, TokenType.KW_MASTER,
1570
+ TokenType.KW_FORK, TokenType.KW_SIMD, TokenType.KW_INLINE,
1571
+ TokenType.KW_PURE, TokenType.KW_EXTERN, TokenType.KW_NATIVE,
1572
+ TokenType.KW_WASM, TokenType.KW_MACRO, TokenType.KW_ASM,
1573
+ TokenType.KW_VOLATILE, TokenType.KW_ALIGN, TokenType.KW_SIZEOF,
1574
+ TokenType.KW_NAN, TokenType.KW_INF, TokenType.KW_PI,
1575
+ TokenType.KW_SQRT, TokenType.KW_POW, TokenType.KW_ABS,
1576
+ TokenType.KW_FLOOR, TokenType.KW_CEIL, TokenType.KW_ROUND,
1577
+ TokenType.KW_SIN, TokenType.KW_COS, TokenType.KW_TAN,
1578
+ TokenType.KW_ASIN, TokenType.KW_ACOS, TokenType.KW_ATAN,
1579
+ TokenType.KW_ATAN2, TokenType.KW_EXP, TokenType.KW_RANDOM,
1580
+ TokenType.KW_SEED, TokenType.KW_MIN, TokenType.KW_MAX,
1581
+ TokenType.KW_CLAMP, TokenType.KW_LERP, TokenType.KW_NORMALIZE,
1582
+ TokenType.KW_DISTANCE, TokenType.KW_MAGNITUDE, TokenType.KW_DETERMINANT,
1583
+ TokenType.KW_INVERSE, TokenType.KW_TRANSPOSE, TokenType.KW_GRADIENT,
1584
+ TokenType.KW_INTEGRATE, TokenType.KW_DIFFERENTIATE, TokenType.KW_SOLVE,
1585
+ TokenType.KW_OPTIMIZE, TokenType.KW_MINIMIZE, TokenType.KW_MAXIMIZE,
1586
+ TokenType.KW_TRAIN, TokenType.KW_PREDICT, TokenType.KW_SIMULATE,
1587
+ TokenType.KW_LOSS, TokenType.KW_BACKPROP, TokenType.KW_DENSE,
1588
+ TokenType.KW_CONV, TokenType.KW_POOL, TokenType.KW_DROPOUT,
1589
+ TokenType.KW_ACTIVATION,TokenType.KW_RELU, TokenType.KW_SIGMOID,
1590
+ TokenType.KW_SOFTMAX, TokenType.KW_ATTENTION, TokenType.KW_TRANSFORMER,
1591
+ TokenType.KW_EMBEDDING, TokenType.KW_TOKENIZE, TokenType.KW_DATASET,
1592
+ TokenType.KW_BATCH, TokenType.KW_EPOCH, TokenType.KW_OPTIMIZER,
1593
+ TokenType.KW_SGD, TokenType.KW_ADAM, TokenType.KW_MOMENTUM,
1594
+ TokenType.KW_REGULARIZE,TokenType.KW_CHECKPOINT, TokenType.KW_INFERENCE,
1595
+ TokenType.KW_ACCURACY, TokenType.KW_RECALL, TokenType.KW_FEATURE,
1596
+ TokenType.KW_LABEL, TokenType.KW_ENSEMBLE, TokenType.KW_BOOST,
1597
+ TokenType.KW_KMEANS, TokenType.KW_PCA, TokenType.KW_SVM,
1598
+ TokenType.KW_REGRESSION,TokenType.KW_CLASSIFICATION, TokenType.KW_DETECTION,
1599
+ TokenType.KW_SEGMENTATION, TokenType.KW_GENERATION, TokenType.KW_DIFFUSION,
1600
+ TokenType.KW_GAN, TokenType.KW_VAE, TokenType.KW_REINFORCE,
1601
+ TokenType.KW_REWARD, TokenType.KW_ENVIRONMENT, TokenType.KW_ACTION,
1602
+ TokenType.KW_OBSERVATION, TokenType.KW_POLICY, TokenType.KW_VALUE,
1603
+ TokenType.KW_RIGIDBODY, TokenType.KW_SOFTBODY, TokenType.KW_FLUID,
1604
+ TokenType.KW_SPRING, TokenType.KW_DAMPER, TokenType.KW_FRICTION,
1605
+ TokenType.KW_RESTITUTION, TokenType.KW_MASS, TokenType.KW_INERTIA,
1606
+ TokenType.KW_VELOCITY, TokenType.KW_ACCELERATION,TokenType.KW_TORQUE,
1607
+ TokenType.KW_RAYCAST, TokenType.KW_IMPULSE, TokenType.KW_TIMESTEP,
1608
+ TokenType.KW_SUBSTEP, TokenType.KW_ENTITY, TokenType.KW_WORLD,
1609
+ TokenType.KW_RESOURCE, TokenType.KW_EVENT, TokenType.KW_COMMAND,
1610
+ TokenType.KW_PLUGIN, TokenType.KW_STAGE, TokenType.KW_ORDERED,
1611
+ TokenType.KW_WITH, TokenType.KW_WITHOUT, TokenType.KW_MAYBE,
1612
+ TokenType.KW_LOG, TokenType.KW_WARN, TokenType.KW_ERROR,
1613
+ TokenType.KW_DEBUG, TokenType.KW_INFO,
1614
+ TokenType.KW_MATRIX, TokenType.KW_VECTOR, TokenType.KW_TENSOR,
1615
+ TokenType.KW_PARTICLE, TokenType.KW_FORCE, TokenType.KW_GRAVITY,
1616
+ TokenType.KW_COLLISION, TokenType.KW_SCENE, TokenType.KW_CANVAS,
1617
+ TokenType.KW_DRAW, TokenType.KW_CLEAR, TokenType.KW_COLOR,
1618
+ TokenType.KW_FILL, TokenType.KW_STROKE, TokenType.KW_IMAGE,
1619
+ TokenType.KW_SPRITE, TokenType.KW_SECURITY, TokenType.KW_ENCRYPT,
1620
+ TokenType.KW_DECRYPT, TokenType.KW_HASH, TokenType.KW_TOKEN,
1621
+ TokenType.KW_AUTH, TokenType.KW_VALIDATE, TokenType.KW_SANITIZE,
1622
+ TokenType.KW_VOID, TokenType.KW_MUT, TokenType.KW_REF,
1623
+ TokenType.KW_CLONE, TokenType.KW_MOVE, TokenType.KW_COPY,
1624
+ TokenType.KW_SHARE, TokenType.KW_DEFER, TokenType.KW_PANIC,
1625
+ TokenType.KW_RECOVER, TokenType.KW_BENCHMARK, TokenType.KW_PROFILE,
1626
+ TokenType.KW_MEASURE, TokenType.KW_CACHE, TokenType.KW_MEMO,
1627
+ TokenType.KW_PROXY, TokenType.KW_REFLECT, TokenType.KW_META,
1628
+ TokenType.KW_DECORATOR, TokenType.KW_INJECT, TokenType.KW_PROVIDE,
1629
+ TokenType.KW_OBSERVE, TokenType.KW_SUBSCRIBE, TokenType.KW_UNSUBSCRIBE,
1630
+ TokenType.KW_BUFFER, TokenType.KW_CHUNK, TokenType.KW_FLUSH,
1631
+ TokenType.KW_COMPRESS, TokenType.KW_DECOMPRESS, TokenType.KW_SERIALIZE,
1632
+ TokenType.KW_DESERIALIZE, TokenType.KW_PARSE, TokenType.KW_FORMAT,
1633
+ TokenType.KW_ENCODE, TokenType.KW_DECODE, TokenType.KW_TRANSFORM,
1634
+ TokenType.KW_FILTER, TokenType.KW_REDUCE, TokenType.KW_SORT,
1635
+ TokenType.KW_FIND, TokenType.KW_SEARCH, TokenType.KW_INDEX,
1636
+ TokenType.KW_SLICE, TokenType.KW_SPLICE, TokenType.KW_SPLIT,
1637
+ TokenType.KW_TRIM, TokenType.KW_UPPER, TokenType.KW_LOWER,
1638
+ TokenType.KW_REPLACE, TokenType.KW_REGEX, TokenType.KW_FILE,
1639
+ TokenType.KW_LAZY, TokenType.KW_EAGER, TokenType.KW_CONCURRENT,
1640
+ TokenType.KW_MUTEX, TokenType.KW_LOCK, TokenType.KW_UNLOCK,
1641
+ TokenType.KW_THREAD, TokenType.KW_SPAWN, TokenType.KW_CHANNEL,
1642
+ TokenType.KW_SEND, TokenType.KW_RECEIVE,
1643
+ // HTML tag keywords as identifiers
1644
+ TokenType.KW_HTML_DIV, TokenType.KW_HTML_SPAN, TokenType.KW_HTML_P,
1645
+ TokenType.KW_HTML_A,
1646
+ ]);
1647
+ return usable.has(tok.type);
1648
+ }
1649
+
1650
+ /** Try to parse identifier primary (and check for arrow function) */
1651
+ _parseIdentifierPrimary() {
1652
+ const tok = this.current();
1653
+ this.advance();
1654
+ return AST.Identifier(tok.value, tok.line, tok.col);
1655
+ }
1656
+
1657
+ /**
1658
+ * Parse `(…)` – either a grouped expression or the start of an arrow fn.
1659
+ * Lookahead heuristic: if after consuming `(params)` we see `=>`, it's an arrow.
1660
+ */
1661
+ _parseGroupedOrArrow() {
1662
+ const tok = this.current();
1663
+ const savedPos = this.pos;
1664
+
1665
+ // Collect the items inside ( ) as potential params
1666
+ this.advance(); // consume `(`
1667
+ this.skipNewlines();
1668
+
1669
+ // Try to parse as arrow function parameters
1670
+ const params = [];
1671
+ let isArrow = false;
1672
+ let isAsync = false;
1673
+ let arrowPos = savedPos;
1674
+
1675
+ try {
1676
+ while (!this.check(TokenType.RPAREN) && !this.check(TokenType.EOF)) {
1677
+ this.skipNewlines();
1678
+ if (this.check(TokenType.SPREAD)) {
1679
+ this.advance();
1680
+ params.push('...' + this._expectIdentifierName('Expected rest param'));
1681
+ } else {
1682
+ const paramTok = this.current();
1683
+ if (paramTok.type !== TokenType.IDENTIFIER && !this._isKeywordUsableAsIdentifier(paramTok)) {
1684
+ throw new Error('not-arrow');
1685
+ }
1686
+ this.advance();
1687
+ const name = paramTok.value;
1688
+ if (this.match(TokenType.ASSIGN)) {
1689
+ const def = this._parseExpression();
1690
+ params.push({ name, default: def });
1691
+ } else {
1692
+ params.push(name);
1693
+ }
1694
+ }
1695
+ this.skipNewlines();
1696
+ if (!this.match(TokenType.COMMA)) break;
1697
+ this.skipNewlines();
1698
+ }
1699
+ this.skipNewlines();
1700
+ this.expect(TokenType.RPAREN, 'Expected )');
1701
+ this.skipNewlines();
1702
+ if (this.check(TokenType.FAT_ARROW)) {
1703
+ isArrow = true;
1704
+ arrowPos = this.pos;
1705
+ }
1706
+ } catch (_) {
1707
+ isArrow = false;
1708
+ }
1709
+
1710
+ if (isArrow) {
1711
+ // Commit: parse arrow body
1712
+ this.advance(); // consume `=>`
1713
+ this.skipNewlines();
1714
+ let body;
1715
+ if (this.check(TokenType.LBRACE)) {
1716
+ // { … } block body
1717
+ this.advance();
1718
+ body = this._parseBlock();
1719
+ this.expect(TokenType.RBRACE, "Expected '}' to close arrow function body");
1720
+ } else if (this.check(TokenType.COLON)) {
1721
+ // ThyScript block style: => : ... end()
1722
+ body = this._expectColonBlock();
1723
+ this._expectEnd();
1724
+ } else {
1725
+ body = this._parseAssignment();
1726
+ }
1727
+ return AST.ArrowFunctionExpr(params, body, isAsync, tok.line, tok.col);
1728
+ }
1729
+
1730
+ // Not an arrow – rewind and parse as grouped expression
1731
+ this.pos = savedPos;
1732
+ this.advance(); // consume `(` again
1733
+ this.skipNewlines();
1734
+ const expr = this._parseExpression();
1735
+ this.skipNewlines();
1736
+ this.expect(TokenType.RPAREN, "Expected ')' to close grouped expression");
1737
+ return expr;
1738
+ }
1739
+
1740
+ /** Array literal [e1, e2, …] */
1741
+ _parseArrayLiteral() {
1742
+ const tok = this.current();
1743
+ this.advance(); // consume `[`
1744
+ const elements = [];
1745
+ this.skipNewlines();
1746
+ while (!this.check(TokenType.RBRACKET) && !this.check(TokenType.EOF)) {
1747
+ if (this.check(TokenType.SPREAD)) {
1748
+ const st = this.current();
1749
+ this.advance();
1750
+ elements.push(AST.SpreadExpr(this._parseExpression(), st.line, st.col));
1751
+ } else if (this.check(TokenType.COMMA)) {
1752
+ elements.push(AST.Literal('undefined', undefined, tok.line, tok.col)); // elision
1753
+ } else {
1754
+ elements.push(this._parseExpression());
1755
+ }
1756
+ this.skipNewlines();
1757
+ if (!this.match(TokenType.COMMA)) break;
1758
+ this.skipNewlines();
1759
+ }
1760
+ this.skipNewlines();
1761
+ this.expect(TokenType.RBRACKET, "Expected ']'");
1762
+ return AST.ArrayExpr(elements, tok.line, tok.col);
1763
+ }
1764
+
1765
+ /** Object literal { key: value, … } */
1766
+ _parseObjectLiteral() {
1767
+ const tok = this.current();
1768
+ this.advance(); // consume `{`
1769
+ const props = [];
1770
+ this.skipNewlines();
1771
+ while (!this.check(TokenType.RBRACE) && !this.check(TokenType.EOF)) {
1772
+ this.skipNewlines();
1773
+
1774
+ // Spread in object
1775
+ if (this.check(TokenType.SPREAD)) {
1776
+ const st = this.current();
1777
+ this.advance();
1778
+ props.push({
1779
+ type: 'spread',
1780
+ argument: this._parseExpression(),
1781
+ line: st.line, col: st.col,
1782
+ });
1783
+ this.skipNewlines();
1784
+ this.match(TokenType.COMMA);
1785
+ this.skipNewlines();
1786
+ continue;
1787
+ }
1788
+
1789
+ // Computed property [expr]: value
1790
+ if (this.check(TokenType.LBRACKET)) {
1791
+ this.advance();
1792
+ const key = this._parseExpression();
1793
+ this.expect(TokenType.RBRACKET, "Expected ']'");
1794
+ this.expect(TokenType.COLON, "Expected ':' after computed key");
1795
+ const value = this._parseExpression();
1796
+ props.push({ key, value, computed: true, shorthand: false });
1797
+ this.skipNewlines();
1798
+ this.match(TokenType.COMMA);
1799
+ this.skipNewlines();
1800
+ continue;
1801
+ }
1802
+
1803
+ // Method shorthand name(params): body end() or name(params) { body }
1804
+ // Detected by: identifier followed by (
1805
+ const keyTok = this.current();
1806
+ const keyName = this._extractPropertyKey();
1807
+
1808
+ if (this.check(TokenType.LPAREN)) {
1809
+ // Method
1810
+ const params = this._parseParamList();
1811
+ let body;
1812
+ if (this.check(TokenType.LBRACE)) {
1813
+ this.advance();
1814
+ body = this._parseBlock();
1815
+ this.expect(TokenType.RBRACE, "Expected '}'");
1816
+ } else {
1817
+ body = this._expectColonBlock();
1818
+ this._expectEnd();
1819
+ }
1820
+ props.push({
1821
+ key: AST.Literal('string', keyName, keyTok.line, keyTok.col),
1822
+ value: AST.FunctionDecl(keyName, params, body, false, false, keyTok.line, keyTok.col),
1823
+ computed: false,
1824
+ shorthand: false,
1825
+ method: true,
1826
+ });
1827
+ this.skipNewlines();
1828
+ this.match(TokenType.COMMA);
1829
+ this.skipNewlines();
1830
+ continue;
1831
+ }
1832
+
1833
+ // Normal property key: value or shorthand key
1834
+ if (this.match(TokenType.COLON)) {
1835
+ const value = this._parseExpression();
1836
+ props.push({
1837
+ key: AST.Literal('string', keyName, keyTok.line, keyTok.col),
1838
+ value,
1839
+ computed: false,
1840
+ shorthand: false,
1841
+ });
1842
+ } else {
1843
+ // Shorthand { x } → { x: x }
1844
+ props.push({
1845
+ key: AST.Literal('string', keyName, keyTok.line, keyTok.col),
1846
+ value: AST.Identifier(keyName, keyTok.line, keyTok.col),
1847
+ computed: false,
1848
+ shorthand: true,
1849
+ });
1850
+ }
1851
+
1852
+ this.skipNewlines();
1853
+ if (!this.match(TokenType.COMMA)) break;
1854
+ this.skipNewlines();
1855
+ }
1856
+ this.skipNewlines();
1857
+ this.expect(TokenType.RBRACE, "Expected '}' to close object literal");
1858
+ return AST.ObjectExpr(props, tok.line, tok.col);
1859
+ }
1860
+
1861
+ /** Extract a property key as a string (identifier, string literal, number) */
1862
+ _extractPropertyKey() {
1863
+ const tok = this.current();
1864
+ if (tok.type === TokenType.STRING) {
1865
+ this.advance();
1866
+ return tok.value;
1867
+ }
1868
+ if (tok.type === TokenType.NUMBER || tok.type === TokenType.FLOAT) {
1869
+ this.advance();
1870
+ return String(tok.value);
1871
+ }
1872
+ return this._expectIdentifierName('Expected property key');
1873
+ }
1874
+
1875
+ // ═══════════════════════════════════════════════════════════════════════════
1876
+ // Utility helpers
1877
+ // ═══════════════════════════════════════════════════════════════════════════
1878
+
1879
+ /**
1880
+ * Consume the current `end` token and the `()` that follows it.
1881
+ * ThyScript uses `end()` as block terminator.
1882
+ */
1883
+ _expectEnd() {
1884
+ this.skipNewlines();
1885
+ if (this.check(TokenType.KW_END)) {
1886
+ this.advance(); // consume `end`
1887
+ this.skipNewlines();
1888
+ // Consume optional ()
1889
+ if (this.check(TokenType.LPAREN)) {
1890
+ this.advance();
1891
+ this.skipNewlines();
1892
+ if (this.check(TokenType.RPAREN)) this.advance();
1893
+ }
1894
+ } else {
1895
+ this._error("Expected 'end()' to close block", this.current());
1896
+ }
1897
+ }
1898
+
1899
+ /**
1900
+ * Consume the next token as an identifier name.
1901
+ * Accepts both IDENTIFIER tokens and many keyword tokens that double as names.
1902
+ */
1903
+ _expectIdentifierName(errorMsg) {
1904
+ this.skipNewlines();
1905
+ const tok = this.current();
1906
+ if (tok.type === TokenType.IDENTIFIER) {
1907
+ this.advance();
1908
+ return tok.value;
1909
+ }
1910
+ if (this._isKeywordUsableAsIdentifier(tok)) {
1911
+ this.advance();
1912
+ return tok.value;
1913
+ }
1914
+ this._error(errorMsg || 'Expected identifier', tok);
1915
+ return '_error_';
1916
+ }
1917
+
1918
+ /** Consume a STRING token and return its value */
1919
+ _expectString(errorMsg) {
1920
+ this.skipNewlines();
1921
+ const tok = this.current();
1922
+ if (tok.type === TokenType.STRING) {
1923
+ this.advance();
1924
+ return tok.value;
1925
+ }
1926
+ if (tok.type === TokenType.TEMPLATE_LITERAL) {
1927
+ this.advance();
1928
+ return tok.value;
1929
+ }
1930
+ this._error(errorMsg || 'Expected string', tok);
1931
+ return '';
1932
+ }
1933
+ }
1934
+
1935
+ // ─────────────────────────────────────────────────────────────────────────────
1936
+ // Module-level helpers (avoid `this` inside switch/static context)
1937
+ // ─────────────────────────────────────────────────────────────────────────────
1938
+ function checkAny(tok, ...types) {
1939
+ return types.includes(tok.type);
1940
+ }
1941
+
1942
+ module.exports = Parser;