tova 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tova.js +438 -58
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +170 -29
- package/src/analyzer/client-analyzer.js +21 -5
- package/src/analyzer/scope.js +78 -3
- package/src/codegen/base-codegen.js +754 -45
- package/src/codegen/client-codegen.js +293 -36
- package/src/codegen/codegen.js +6 -1
- package/src/codegen/server-codegen.js +189 -40
- package/src/codegen/wasm-codegen.js +610 -0
- package/src/lexer/lexer.js +157 -109
- package/src/lexer/tokens.js +3 -0
- package/src/lsp/server.js +148 -12
- package/src/parser/ast.js +2 -1
- package/src/parser/client-parser.js +10 -3
- package/src/parser/parser.js +142 -148
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +307 -59
- package/src/runtime/ssr.js +101 -34
- package/src/stdlib/inline.js +333 -24
- package/src/stdlib/native-bridge.js +150 -0
- package/src/version.js +1 -1
package/src/lsp/server.js
CHANGED
|
@@ -145,7 +145,7 @@ class TovaLanguageServer {
|
|
|
145
145
|
capabilities: {
|
|
146
146
|
textDocumentSync: {
|
|
147
147
|
openClose: true,
|
|
148
|
-
change:
|
|
148
|
+
change: 2, // Incremental sync — receive only changed ranges
|
|
149
149
|
save: { includeText: true },
|
|
150
150
|
},
|
|
151
151
|
completionProvider: {
|
|
@@ -183,11 +183,56 @@ class TovaLanguageServer {
|
|
|
183
183
|
|
|
184
184
|
_onDidChange(params) {
|
|
185
185
|
const { uri, version } = params.textDocument;
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
const changes = params.contentChanges;
|
|
187
|
+
if (!changes || changes.length === 0) return;
|
|
188
|
+
|
|
189
|
+
const doc = this._documents.get(uri);
|
|
190
|
+
if (!doc) return;
|
|
191
|
+
|
|
192
|
+
let text = doc.text;
|
|
193
|
+
|
|
194
|
+
for (const change of changes) {
|
|
195
|
+
if (change.range) {
|
|
196
|
+
// Incremental sync: apply range edit
|
|
197
|
+
text = this._applyEdit(text, change.range, change.text);
|
|
198
|
+
} else {
|
|
199
|
+
// Full sync fallback (e.g., if client sends full text)
|
|
200
|
+
text = change.text;
|
|
201
|
+
}
|
|
190
202
|
}
|
|
203
|
+
|
|
204
|
+
this._documents.set(uri, { text, version });
|
|
205
|
+
this._debouncedValidate(uri, text);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Apply a single incremental text edit to the document
|
|
209
|
+
_applyEdit(text, range, newText) {
|
|
210
|
+
const startOffset = this._positionToOffset(text, range.start);
|
|
211
|
+
const endOffset = this._positionToOffset(text, range.end);
|
|
212
|
+
return text.slice(0, startOffset) + newText + text.slice(endOffset);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Convert { line, character } position to byte offset in text
|
|
216
|
+
_positionToOffset(text, pos) {
|
|
217
|
+
let line = 0;
|
|
218
|
+
let offset = 0;
|
|
219
|
+
while (line < pos.line && offset < text.length) {
|
|
220
|
+
if (text[offset] === '\n') line++;
|
|
221
|
+
offset++;
|
|
222
|
+
}
|
|
223
|
+
return offset + pos.character;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_debouncedValidate(uri, text) {
|
|
227
|
+
if (!this._validateTimers) this._validateTimers = new Map();
|
|
228
|
+
const existing = this._validateTimers.get(uri);
|
|
229
|
+
if (existing) clearTimeout(existing);
|
|
230
|
+
this._validateTimers.set(uri, setTimeout(() => {
|
|
231
|
+
this._validateTimers.delete(uri);
|
|
232
|
+
// Re-read latest text in case more changes arrived
|
|
233
|
+
const doc = this._documents.get(uri);
|
|
234
|
+
if (doc) this._validateDocument(uri, doc.text);
|
|
235
|
+
}, 200));
|
|
191
236
|
}
|
|
192
237
|
|
|
193
238
|
_onDidClose(params) {
|
|
@@ -222,6 +267,8 @@ class TovaLanguageServer {
|
|
|
222
267
|
// LSP always runs with strict: true for better diagnostics
|
|
223
268
|
const analyzer = new Analyzer(ast, filename, { strict: true });
|
|
224
269
|
const result = analyzer.analyze();
|
|
270
|
+
// Build sorted scope index for O(log n) position lookups
|
|
271
|
+
analyzer.globalScope.buildIndex();
|
|
225
272
|
const { warnings } = result;
|
|
226
273
|
const typeRegistry = TypeRegistry.fromAnalyzer(analyzer);
|
|
227
274
|
|
|
@@ -296,6 +343,7 @@ class TovaLanguageServer {
|
|
|
296
343
|
try {
|
|
297
344
|
const analyzer = new Analyzer(partialAST, filename, { tolerant: true, strict: true });
|
|
298
345
|
const result = analyzer.analyze();
|
|
346
|
+
analyzer.globalScope.buildIndex();
|
|
299
347
|
const typeRegistry = TypeRegistry.fromAnalyzer(analyzer);
|
|
300
348
|
this._diagnosticsCache.set(uri, { ast: partialAST, analyzer, text, typeRegistry });
|
|
301
349
|
for (const w of result.warnings) {
|
|
@@ -1555,7 +1603,7 @@ class TovaLanguageServer {
|
|
|
1555
1603
|
});
|
|
1556
1604
|
}
|
|
1557
1605
|
|
|
1558
|
-
// ─── References
|
|
1606
|
+
// ─── References (scope-aware) ───────────────────────────
|
|
1559
1607
|
|
|
1560
1608
|
_onReferences(msg) {
|
|
1561
1609
|
const { position, textDocument } = msg.params;
|
|
@@ -1566,15 +1614,90 @@ class TovaLanguageServer {
|
|
|
1566
1614
|
const word = this._getWordAt(line, position.character);
|
|
1567
1615
|
if (!word) return this._respond(msg.id, []);
|
|
1568
1616
|
|
|
1617
|
+
const cached = this._diagnosticsCache.get(textDocument.uri);
|
|
1618
|
+
if (!cached || !cached.analyzer || !cached.analyzer.globalScope) {
|
|
1619
|
+
// Fallback to naive text search if no scope info
|
|
1620
|
+
return this._naiveReferences(msg.id, textDocument.uri, doc.text, word);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// Find the scope at cursor and walk up to find the defining scope
|
|
1624
|
+
const cursorLine = position.line + 1;
|
|
1625
|
+
const cursorCol = position.character + 1;
|
|
1626
|
+
const cursorScope = cached.analyzer.globalScope.findScopeAtPosition(cursorLine, cursorCol);
|
|
1627
|
+
if (!cursorScope) {
|
|
1628
|
+
return this._naiveReferences(msg.id, textDocument.uri, doc.text, word);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
let definingScope = null;
|
|
1632
|
+
let scope = cursorScope;
|
|
1633
|
+
while (scope) {
|
|
1634
|
+
if (scope.symbols && scope.symbols.has(word)) {
|
|
1635
|
+
definingScope = scope;
|
|
1636
|
+
break;
|
|
1637
|
+
}
|
|
1638
|
+
scope = scope.parent;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
if (!definingScope) {
|
|
1642
|
+
return this._naiveReferences(msg.id, textDocument.uri, doc.text, word);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// Collect locations: for each text occurrence, check if it resolves to the same scope
|
|
1569
1646
|
const locations = [];
|
|
1570
1647
|
const docLines = doc.text.split('\n');
|
|
1571
|
-
const
|
|
1648
|
+
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1649
|
+
const wordRegex = new RegExp('\\b' + escaped + '\\b', 'g');
|
|
1650
|
+
|
|
1651
|
+
for (let i = 0; i < docLines.length; i++) {
|
|
1652
|
+
let match;
|
|
1653
|
+
while ((match = wordRegex.exec(docLines[i])) !== null) {
|
|
1654
|
+
const matchLine = i + 1;
|
|
1655
|
+
const matchCol = match.index + 1;
|
|
1656
|
+
const matchScope = cached.analyzer.globalScope.findScopeAtPosition(matchLine, matchCol);
|
|
1657
|
+
if (!matchScope) continue;
|
|
1658
|
+
|
|
1659
|
+
// Walk up from matchScope to see if the name resolves to definingScope
|
|
1660
|
+
let resolvedScope = null;
|
|
1661
|
+
let s = matchScope;
|
|
1662
|
+
while (s) {
|
|
1663
|
+
if (s.symbols && s.symbols.has(word)) {
|
|
1664
|
+
resolvedScope = s;
|
|
1665
|
+
break;
|
|
1666
|
+
}
|
|
1667
|
+
s = s.parent;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
if (resolvedScope === definingScope) {
|
|
1671
|
+
locations.push({
|
|
1672
|
+
uri: textDocument.uri,
|
|
1673
|
+
range: {
|
|
1674
|
+
start: { line: i, character: match.index },
|
|
1675
|
+
end: { line: i, character: match.index + word.length },
|
|
1676
|
+
},
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Fallback if scope-aware search found nothing (e.g. positional info gaps)
|
|
1683
|
+
if (locations.length === 0) {
|
|
1684
|
+
return this._naiveReferences(msg.id, textDocument.uri, doc.text, word);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
this._respond(msg.id, locations);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
_naiveReferences(id, uri, text, word) {
|
|
1691
|
+
const locations = [];
|
|
1692
|
+
const docLines = text.split('\n');
|
|
1693
|
+
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1694
|
+
const wordRegex = new RegExp('\\b' + escaped + '\\b', 'g');
|
|
1572
1695
|
|
|
1573
1696
|
for (let i = 0; i < docLines.length; i++) {
|
|
1574
1697
|
let match;
|
|
1575
1698
|
while ((match = wordRegex.exec(docLines[i])) !== null) {
|
|
1576
1699
|
locations.push({
|
|
1577
|
-
uri:
|
|
1700
|
+
uri: uri,
|
|
1578
1701
|
range: {
|
|
1579
1702
|
start: { line: i, character: match.index },
|
|
1580
1703
|
end: { line: i, character: match.index + word.length },
|
|
@@ -1583,7 +1706,7 @@ class TovaLanguageServer {
|
|
|
1583
1706
|
}
|
|
1584
1707
|
}
|
|
1585
1708
|
|
|
1586
|
-
this._respond(
|
|
1709
|
+
this._respond(id, locations);
|
|
1587
1710
|
}
|
|
1588
1711
|
|
|
1589
1712
|
// ─── Workspace Symbol ──────────────────────────────────
|
|
@@ -1869,6 +1992,19 @@ class TovaLanguageServer {
|
|
|
1869
1992
|
}
|
|
1870
1993
|
}
|
|
1871
1994
|
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1995
|
+
export { TovaLanguageServer };
|
|
1996
|
+
|
|
1997
|
+
// Auto-start when imported (for `tova lsp` command).
|
|
1998
|
+
// Use startServer() export for explicit control.
|
|
1999
|
+
export function startServer() {
|
|
2000
|
+
const server = new TovaLanguageServer();
|
|
2001
|
+
server.start();
|
|
2002
|
+
return server;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
// Start automatically — callers that only want the class import should
|
|
2006
|
+
// use dynamic import with test-aware filtering or call startServer() explicitly
|
|
2007
|
+
const _autoStart = globalThis.__TOVA_LSP_NO_AUTOSTART !== true;
|
|
2008
|
+
if (_autoStart) {
|
|
2009
|
+
startServer();
|
|
2010
|
+
}
|
package/src/parser/ast.js
CHANGED
|
@@ -77,7 +77,7 @@ export class LetDestructure {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export class FunctionDeclaration {
|
|
80
|
-
constructor(name, params, body, returnType, loc, isAsync = false, typeParams = []) {
|
|
80
|
+
constructor(name, params, body, returnType, loc, isAsync = false, typeParams = [], decorators = []) {
|
|
81
81
|
this.type = 'FunctionDeclaration';
|
|
82
82
|
this.name = name;
|
|
83
83
|
this.typeParams = typeParams; // Array of type parameter names (generics)
|
|
@@ -85,6 +85,7 @@ export class FunctionDeclaration {
|
|
|
85
85
|
this.body = body; // BlockStatement or Expression (implicit return)
|
|
86
86
|
this.returnType = returnType; // optional type annotation
|
|
87
87
|
this.isAsync = isAsync;
|
|
88
|
+
this.decorators = decorators; // Array of { name, args } for @decorator syntax
|
|
88
89
|
this.loc = loc;
|
|
89
90
|
}
|
|
90
91
|
}
|
|
@@ -293,6 +293,13 @@ export function installClientParser(ParserClass) {
|
|
|
293
293
|
suffix = this.expect(TokenType.IDENTIFIER, "Expected name after ':'").value;
|
|
294
294
|
}
|
|
295
295
|
name = `${name}:${suffix}`;
|
|
296
|
+
// Consume event modifiers: on:click.stop.prevent
|
|
297
|
+
if (name.startsWith('on:') && this.check(TokenType.DOT)) {
|
|
298
|
+
while (this.match(TokenType.DOT)) {
|
|
299
|
+
const mod = this.expect(TokenType.IDENTIFIER, "Expected modifier name after '.'").value;
|
|
300
|
+
name += `.${mod}`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
296
303
|
}
|
|
297
304
|
|
|
298
305
|
if (!this.match(TokenType.ASSIGN)) {
|
|
@@ -402,17 +409,17 @@ export function installClientParser(ParserClass) {
|
|
|
402
409
|
if (!this.match(TokenType.COMMA)) break;
|
|
403
410
|
}
|
|
404
411
|
this.expect(TokenType.RBRACKET, "Expected ']' in destructuring pattern");
|
|
405
|
-
variable =
|
|
412
|
+
variable = new AST.ArrayPattern(elements, l);
|
|
406
413
|
} else if (this.check(TokenType.LBRACE)) {
|
|
407
414
|
// Object destructuring: {name, age}
|
|
408
415
|
this.advance(); // consume {
|
|
409
416
|
const props = [];
|
|
410
417
|
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
411
|
-
props.push(this.expect(TokenType.IDENTIFIER, "Expected property name in object pattern").value);
|
|
418
|
+
props.push({ key: this.expect(TokenType.IDENTIFIER, "Expected property name in object pattern").value });
|
|
412
419
|
if (!this.match(TokenType.COMMA)) break;
|
|
413
420
|
}
|
|
414
421
|
this.expect(TokenType.RBRACE, "Expected '}' in destructuring pattern");
|
|
415
|
-
variable =
|
|
422
|
+
variable = new AST.ObjectPattern(props, l);
|
|
416
423
|
} else {
|
|
417
424
|
variable = this.expect(TokenType.IDENTIFIER, "Expected loop variable").value;
|
|
418
425
|
}
|
package/src/parser/parser.js
CHANGED
|
@@ -5,35 +5,25 @@ import { installClientParser } from './client-parser.js';
|
|
|
5
5
|
|
|
6
6
|
export class Parser {
|
|
7
7
|
static MAX_EXPRESSION_DEPTH = 200;
|
|
8
|
+
static COMPARISON_OPS = null; // initialized after class definition
|
|
8
9
|
|
|
9
10
|
constructor(tokens, filename = '<stdin>') {
|
|
10
|
-
|
|
11
|
+
// Pre-filter: build array of significant tokens for O(1) peek
|
|
12
|
+
const significant = [];
|
|
13
|
+
const docs = [];
|
|
14
|
+
for (const t of tokens) {
|
|
15
|
+
const type = t.type;
|
|
16
|
+
if (type === TokenType.NEWLINE || type === TokenType.SEMICOLON) continue;
|
|
17
|
+
if (type === TokenType.DOCSTRING) { docs.push(t); continue; }
|
|
18
|
+
significant.push(t);
|
|
19
|
+
}
|
|
20
|
+
this.tokens = significant;
|
|
21
|
+
this._eof = significant[significant.length - 1]; // cache EOF for hot-path methods
|
|
11
22
|
this.filename = filename;
|
|
12
23
|
this.pos = 0;
|
|
13
24
|
this.errors = [];
|
|
14
25
|
this._expressionDepth = 0;
|
|
15
|
-
this.docstrings =
|
|
16
|
-
this._skipInsignificant();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
_isInsignificant(type) {
|
|
20
|
-
return type === TokenType.NEWLINE || type === TokenType.DOCSTRING || type === TokenType.SEMICOLON;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
_skipInsignificant() {
|
|
24
|
-
while (this.pos < this.tokens.length && this._isInsignificant(this.tokens[this.pos].type)) {
|
|
25
|
-
this.pos++;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
extractDocstrings(tokens) {
|
|
30
|
-
const docs = [];
|
|
31
|
-
for (const t of tokens) {
|
|
32
|
-
if (t.type === TokenType.DOCSTRING) {
|
|
33
|
-
docs.push(t);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return docs;
|
|
26
|
+
this.docstrings = docs;
|
|
37
27
|
}
|
|
38
28
|
|
|
39
29
|
// ─── Helpers ───────────────────────────────────────────────
|
|
@@ -49,28 +39,16 @@ export class Parser {
|
|
|
49
39
|
}
|
|
50
40
|
|
|
51
41
|
current() {
|
|
52
|
-
return this.tokens[this.pos] || this.
|
|
42
|
+
return this.tokens[this.pos] || this._eof;
|
|
53
43
|
}
|
|
54
44
|
|
|
55
45
|
peek(offset = 0) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// General path: skip over insignificant tokens
|
|
59
|
-
let count = 0;
|
|
60
|
-
for (let idx = this.pos + 1; idx < this.tokens.length; idx++) {
|
|
61
|
-
if (!this._isInsignificant(this.tokens[idx].type)) {
|
|
62
|
-
count++;
|
|
63
|
-
if (count === offset) return this.tokens[idx];
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return this.tokens[this.tokens.length - 1];
|
|
46
|
+
const idx = this.pos + offset;
|
|
47
|
+
return idx < this.tokens.length ? this.tokens[idx] : this._eof;
|
|
67
48
|
}
|
|
68
49
|
|
|
69
50
|
advance() {
|
|
70
|
-
|
|
71
|
-
this.pos++;
|
|
72
|
-
this._skipInsignificant();
|
|
73
|
-
return tok;
|
|
51
|
+
return this.tokens[this.pos++] || this._eof;
|
|
74
52
|
}
|
|
75
53
|
|
|
76
54
|
check(type) {
|
|
@@ -250,8 +228,8 @@ export class Parser {
|
|
|
250
228
|
}
|
|
251
229
|
|
|
252
230
|
_attachDocstrings(program) {
|
|
253
|
-
//
|
|
254
|
-
const docTokens = this.
|
|
231
|
+
// Use pre-extracted docstring tokens
|
|
232
|
+
const docTokens = this.docstrings;
|
|
255
233
|
if (docTokens.length === 0) return;
|
|
256
234
|
|
|
257
235
|
// Group consecutive docstring lines
|
|
@@ -563,6 +541,7 @@ export class Parser {
|
|
|
563
541
|
this.advance(); // consume async
|
|
564
542
|
return this.parseForStatement(null, true);
|
|
565
543
|
}
|
|
544
|
+
if (this.check(TokenType.AT)) return this.parseDecoratedDeclaration();
|
|
566
545
|
if (this.check(TokenType.ASYNC) && this.peek(1).type === TokenType.FN) return this.parseAsyncFunctionDeclaration();
|
|
567
546
|
if (this.check(TokenType.FN) && (this.peek(1).type === TokenType.IDENTIFIER || this._isContextualKeywordToken(this.peek(1)))) return this.parseFunctionDeclaration();
|
|
568
547
|
if (this.check(TokenType.TYPE)) return this.parseTypeDeclaration();
|
|
@@ -723,7 +702,35 @@ export class Parser {
|
|
|
723
702
|
return new AST.ExternDeclaration(name, params, returnType, l, isAsync);
|
|
724
703
|
}
|
|
725
704
|
|
|
726
|
-
|
|
705
|
+
parseDecoratedDeclaration() {
|
|
706
|
+
const decorators = [];
|
|
707
|
+
while (this.check(TokenType.AT)) {
|
|
708
|
+
this.advance(); // consume @
|
|
709
|
+
const decName = this.expect(TokenType.IDENTIFIER, "Expected decorator name after '@'").value;
|
|
710
|
+
let decArgs = [];
|
|
711
|
+
if (this.check(TokenType.LPAREN)) {
|
|
712
|
+
this.advance(); // consume (
|
|
713
|
+
while (!this.check(TokenType.RPAREN) && !this.isAtEnd()) {
|
|
714
|
+
decArgs.push(this.parseExpression());
|
|
715
|
+
if (!this.match(TokenType.COMMA)) break;
|
|
716
|
+
}
|
|
717
|
+
this.expect(TokenType.RPAREN, "Expected ')' after decorator arguments");
|
|
718
|
+
}
|
|
719
|
+
decorators.push({ name: decName, args: decArgs });
|
|
720
|
+
}
|
|
721
|
+
// After decorators, expect fn or async fn
|
|
722
|
+
if (this.check(TokenType.ASYNC) && this.peek(1).type === TokenType.FN) {
|
|
723
|
+
const node = this.parseAsyncFunctionDeclaration(decorators);
|
|
724
|
+
return node;
|
|
725
|
+
}
|
|
726
|
+
if (this.check(TokenType.FN)) {
|
|
727
|
+
const node = this.parseFunctionDeclaration(decorators);
|
|
728
|
+
return node;
|
|
729
|
+
}
|
|
730
|
+
this.error("Expected 'fn' or 'async fn' after decorator");
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
parseFunctionDeclaration(decorators = []) {
|
|
727
734
|
const l = this.loc();
|
|
728
735
|
this.expect(TokenType.FN);
|
|
729
736
|
let name;
|
|
@@ -754,10 +761,10 @@ export class Parser {
|
|
|
754
761
|
}
|
|
755
762
|
|
|
756
763
|
const body = this.parseBlock();
|
|
757
|
-
return new AST.FunctionDeclaration(name, params, body, returnType, l, false, typeParams);
|
|
764
|
+
return new AST.FunctionDeclaration(name, params, body, returnType, l, false, typeParams, decorators);
|
|
758
765
|
}
|
|
759
766
|
|
|
760
|
-
parseAsyncFunctionDeclaration() {
|
|
767
|
+
parseAsyncFunctionDeclaration(decorators = []) {
|
|
761
768
|
const l = this.loc();
|
|
762
769
|
this.expect(TokenType.ASYNC);
|
|
763
770
|
this.expect(TokenType.FN);
|
|
@@ -789,7 +796,7 @@ export class Parser {
|
|
|
789
796
|
}
|
|
790
797
|
|
|
791
798
|
const body = this.parseBlock();
|
|
792
|
-
return new AST.FunctionDeclaration(name, params, body, returnType, l, true, typeParams);
|
|
799
|
+
return new AST.FunctionDeclaration(name, params, body, returnType, l, true, typeParams, decorators);
|
|
793
800
|
}
|
|
794
801
|
|
|
795
802
|
parseBreakStatement() {
|
|
@@ -1495,10 +1502,10 @@ export class Parser {
|
|
|
1495
1502
|
// ─── Expressions (precedence climbing) ────────────────────
|
|
1496
1503
|
|
|
1497
1504
|
parseExpression() {
|
|
1498
|
-
if (
|
|
1499
|
-
this._expressionDepth--;
|
|
1505
|
+
if (this._expressionDepth >= Parser.MAX_EXPRESSION_DEPTH) {
|
|
1500
1506
|
this.error('Expression nested too deeply (max ' + Parser.MAX_EXPRESSION_DEPTH + ' levels)');
|
|
1501
1507
|
}
|
|
1508
|
+
this._expressionDepth++;
|
|
1502
1509
|
try {
|
|
1503
1510
|
return this.parsePipe();
|
|
1504
1511
|
} finally {
|
|
@@ -1578,9 +1585,7 @@ export class Parser {
|
|
|
1578
1585
|
let left = this.parseMembership();
|
|
1579
1586
|
|
|
1580
1587
|
// Check for chained comparisons: a < b < c
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
if (compOps.some(op => this.check(op))) {
|
|
1588
|
+
if (Parser.COMPARISON_OPS.has(this.current().type)) {
|
|
1584
1589
|
// Don't parse < as comparison if it looks like JSX
|
|
1585
1590
|
if (this.check(TokenType.LESS) && this._looksLikeJSX()) {
|
|
1586
1591
|
return left;
|
|
@@ -1589,9 +1594,8 @@ export class Parser {
|
|
|
1589
1594
|
const operands = [left];
|
|
1590
1595
|
const operators = [];
|
|
1591
1596
|
|
|
1592
|
-
while (
|
|
1593
|
-
const op = this.
|
|
1594
|
-
if (!op) break;
|
|
1597
|
+
while (Parser.COMPARISON_OPS.has(this.current().type)) {
|
|
1598
|
+
const op = this.advance();
|
|
1595
1599
|
operators.push(op.value);
|
|
1596
1600
|
operands.push(this.parseMembership());
|
|
1597
1601
|
}
|
|
@@ -1885,117 +1889,100 @@ export class Parser {
|
|
|
1885
1889
|
|
|
1886
1890
|
parsePrimary() {
|
|
1887
1891
|
const l = this.loc();
|
|
1892
|
+
const tokenType = this.current().type;
|
|
1888
1893
|
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
}
|
|
1894
|
+
switch (tokenType) {
|
|
1895
|
+
case TokenType.NUMBER:
|
|
1896
|
+
return new AST.NumberLiteral(this.advance().value, l);
|
|
1893
1897
|
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
// Regex literal
|
|
1900
|
-
if (this.check(TokenType.REGEX)) {
|
|
1901
|
-
const token = this.advance();
|
|
1902
|
-
return new AST.RegexLiteral(token.value.pattern, token.value.flags, l);
|
|
1903
|
-
}
|
|
1898
|
+
case TokenType.STRING:
|
|
1899
|
+
case TokenType.STRING_TEMPLATE:
|
|
1900
|
+
return this.parseStringLiteral();
|
|
1904
1901
|
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
}
|
|
1910
|
-
if (this.check(TokenType.FALSE)) {
|
|
1911
|
-
this.advance();
|
|
1912
|
-
return new AST.BooleanLiteral(false, l);
|
|
1913
|
-
}
|
|
1902
|
+
case TokenType.REGEX: {
|
|
1903
|
+
const token = this.advance();
|
|
1904
|
+
return new AST.RegexLiteral(token.value.pattern, token.value.flags, l);
|
|
1905
|
+
}
|
|
1914
1906
|
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
return new AST.NilLiteral(l);
|
|
1919
|
-
}
|
|
1907
|
+
case TokenType.TRUE:
|
|
1908
|
+
this.advance();
|
|
1909
|
+
return new AST.BooleanLiteral(true, l);
|
|
1920
1910
|
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
}
|
|
1911
|
+
case TokenType.FALSE:
|
|
1912
|
+
this.advance();
|
|
1913
|
+
return new AST.BooleanLiteral(false, l);
|
|
1925
1914
|
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
}
|
|
1915
|
+
case TokenType.NIL:
|
|
1916
|
+
this.advance();
|
|
1917
|
+
return new AST.NilLiteral(l);
|
|
1930
1918
|
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
return this.parseAsyncLambda();
|
|
1934
|
-
}
|
|
1919
|
+
case TokenType.MATCH:
|
|
1920
|
+
return this.parseMatchExpression();
|
|
1935
1921
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
return this.parseLambda();
|
|
1939
|
-
}
|
|
1922
|
+
case TokenType.IF:
|
|
1923
|
+
return this.parseIfExpression();
|
|
1940
1924
|
|
|
1941
|
-
|
|
1942
|
-
|
|
1925
|
+
case TokenType.ASYNC:
|
|
1926
|
+
if (this.peek(1).type === TokenType.FN) {
|
|
1927
|
+
return this.parseAsyncLambda();
|
|
1928
|
+
}
|
|
1929
|
+
break;
|
|
1943
1930
|
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1931
|
+
case TokenType.FN:
|
|
1932
|
+
if (this.peek(1).type === TokenType.LPAREN) {
|
|
1933
|
+
return this.parseLambda();
|
|
1934
|
+
}
|
|
1935
|
+
break;
|
|
1936
|
+
|
|
1937
|
+
case TokenType.LBRACKET:
|
|
1938
|
+
return this.parseArrayOrComprehension();
|
|
1939
|
+
|
|
1940
|
+
case TokenType.LBRACE:
|
|
1941
|
+
return this.parseObjectOrDictComprehension();
|
|
1942
|
+
|
|
1943
|
+
case TokenType.DOT:
|
|
1944
|
+
// Column expression: .column (for table operations)
|
|
1945
|
+
if (this.peek(1).type === TokenType.IDENTIFIER) {
|
|
1946
|
+
this.advance(); // consume .
|
|
1947
|
+
const name = this.advance().value; // consume identifier
|
|
1948
|
+
// Check for column assignment: .col = expr (used in derive)
|
|
1949
|
+
if (this.check(TokenType.ASSIGN)) {
|
|
1950
|
+
this.advance(); // consume =
|
|
1951
|
+
const expr = this.parseExpression();
|
|
1952
|
+
return new AST.ColumnAssignment(name, expr, l);
|
|
1953
|
+
}
|
|
1954
|
+
return new AST.ColumnExpression(name, l);
|
|
1955
|
+
}
|
|
1956
|
+
break;
|
|
1948
1957
|
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
return this.parseObjectOrDictComprehension();
|
|
1952
|
-
}
|
|
1958
|
+
case TokenType.LPAREN:
|
|
1959
|
+
return this.parseParenOrArrowLambda();
|
|
1953
1960
|
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
// Column expressions appear in function arguments and assignments
|
|
1960
|
-
this.advance(); // consume .
|
|
1961
|
-
const name = this.advance().value; // consume identifier
|
|
1961
|
+
case TokenType.SERVER:
|
|
1962
|
+
case TokenType.CLIENT:
|
|
1963
|
+
case TokenType.SHARED:
|
|
1964
|
+
case TokenType.DERIVE:
|
|
1965
|
+
return new AST.Identifier(this.advance().value, l);
|
|
1962
1966
|
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1967
|
+
case TokenType.IDENTIFIER: {
|
|
1968
|
+
const name = this.advance().value;
|
|
1969
|
+
// Check for arrow lambda: x => expr or x -> expr
|
|
1970
|
+
if (this.check(TokenType.ARROW) || this.check(TokenType.THIN_ARROW)) {
|
|
1971
|
+
this.advance();
|
|
1972
|
+
const body = this.parseExpression();
|
|
1973
|
+
return new AST.LambdaExpression(
|
|
1974
|
+
[new AST.Parameter(name, null, null, l)],
|
|
1975
|
+
body,
|
|
1976
|
+
l
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
return new AST.Identifier(name, l);
|
|
1968
1980
|
}
|
|
1969
|
-
|
|
1970
|
-
return new AST.ColumnExpression(name, l);
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
// Parenthesized expression or arrow lambda
|
|
1974
|
-
if (this.check(TokenType.LPAREN)) {
|
|
1975
|
-
return this.parseParenOrArrowLambda();
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
// Keywords that can appear as identifiers in expression position
|
|
1979
|
-
if (this.check(TokenType.SERVER) || this.check(TokenType.CLIENT) || this.check(TokenType.SHARED) || this.check(TokenType.DERIVE) ||
|
|
1980
|
-
this._isContextualKeyword()) {
|
|
1981
|
-
const name = this.advance().value;
|
|
1982
|
-
return new AST.Identifier(name, l);
|
|
1983
1981
|
}
|
|
1984
1982
|
|
|
1985
|
-
//
|
|
1986
|
-
if (this.
|
|
1987
|
-
|
|
1988
|
-
// Check for arrow lambda: x => expr or x -> expr
|
|
1989
|
-
if (this.check(TokenType.ARROW) || this.check(TokenType.THIN_ARROW)) {
|
|
1990
|
-
this.advance();
|
|
1991
|
-
const body = this.parseExpression();
|
|
1992
|
-
return new AST.LambdaExpression(
|
|
1993
|
-
[new AST.Parameter(name, null, null, l)],
|
|
1994
|
-
body,
|
|
1995
|
-
l
|
|
1996
|
-
);
|
|
1997
|
-
}
|
|
1998
|
-
return new AST.Identifier(name, l);
|
|
1983
|
+
// Contextual keywords that can appear as identifiers in expression position
|
|
1984
|
+
if (this._isContextualKeyword()) {
|
|
1985
|
+
return new AST.Identifier(this.advance().value, l);
|
|
1999
1986
|
}
|
|
2000
1987
|
|
|
2001
1988
|
this.error(`Unexpected token: ${this.current().type}`);
|
|
@@ -2486,3 +2473,10 @@ export class Parser {
|
|
|
2486
2473
|
return node;
|
|
2487
2474
|
}
|
|
2488
2475
|
}
|
|
2476
|
+
|
|
2477
|
+
// Initialize static Set after class definition (depends on TokenType)
|
|
2478
|
+
Parser.COMPARISON_OPS = new Set([
|
|
2479
|
+
TokenType.LESS, TokenType.LESS_EQUAL,
|
|
2480
|
+
TokenType.GREATER, TokenType.GREATER_EQUAL,
|
|
2481
|
+
TokenType.EQUAL, TokenType.NOT_EQUAL
|
|
2482
|
+
]);
|