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.
- package/bin/thy-node.js +332 -0
- package/bin/thy.js +828 -0
- package/bin/thys.js +25 -0
- package/examples/calculator.thy +274 -0
- package/examples/calculator.thy.js +628 -0
- package/examples/hello.thy +34 -0
- package/examples/login.thy +382 -0
- package/examples/login.thy.js +710 -0
- package/package.json +54 -0
- package/scripts/_class.js +18 -0
- package/scripts/_class2.js +11 -0
- package/scripts/_class3.js +21 -0
- package/scripts/_dstruct.js +20 -0
- package/scripts/_dstruct2.js +24 -0
- package/scripts/_scaffold.js +16 -0
- package/scripts/_scaffold2.js +16 -0
- package/scripts/_scaffold3.js +26 -0
- package/scripts/_tpl_compile.js +13 -0
- package/scripts/_tpl_test.js +9 -0
- package/scripts/_typeof.js +26 -0
- package/scripts/_typeof2.js +25 -0
- package/scripts/patch_genmethod.js +44 -0
- package/scripts/smoke_test.js +167 -0
- package/scripts/smoke_test2.js +151 -0
- package/scripts/test_control_panel.js +58 -0
- package/src/compiler/CodeGenerator.js +1449 -0
- package/src/compiler/JITCompiler.js +325 -0
- package/src/database/DatabaseEngine.js +931 -0
- package/src/errors/ErrorDetector.js +305 -0
- package/src/errors/ErrorMessages.js +497 -0
- package/src/index.js +169 -0
- package/src/lexer/Lexer.js +342 -0
- package/src/lexer/TokenTypes.js +1234 -0
- package/src/memory/MemoryManager.js +283 -0
- package/src/parser/AST.js +606 -0
- package/src/parser/Parser.js +1942 -0
- package/src/runtime/ThyRuntime.js +265 -0
- package/src/runtime/stdlib/ArrayLib.js +207 -0
- package/src/runtime/stdlib/FileLib.js +168 -0
- package/src/runtime/stdlib/MathLib.js +411 -0
- package/src/runtime/stdlib/StringLib.js +387 -0
- package/src/server/ControlPanel.js +1188 -0
- package/src/server/HTMLCSSEngine.js +866 -0
- package/src/server/SecurityEngine.js +714 -0
- package/src/server/ServerEngine.js +738 -0
- package/tests/test1_math.thy +82 -0
- package/tests/test2_oop.thy +133 -0
- package/tests/test3_server.thy +143 -0
- 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;
|