tova 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tova.js +1401 -111
- package/package.json +4 -7
- package/src/analyzer/analyzer.js +831 -709
- package/src/analyzer/client-analyzer.js +191 -0
- package/src/analyzer/server-analyzer.js +467 -0
- package/src/analyzer/types.js +20 -4
- package/src/codegen/base-codegen.js +467 -109
- package/src/codegen/client-codegen.js +92 -42
- package/src/codegen/codegen.js +65 -5
- package/src/codegen/server-codegen.js +290 -36
- package/src/diagnostics/error-codes.js +255 -0
- package/src/diagnostics/formatter.js +150 -28
- package/src/docs/generator.js +390 -0
- package/src/lexer/lexer.js +305 -63
- package/src/lexer/tokens.js +19 -0
- package/src/lsp/server.js +892 -30
- package/src/parser/ast.js +81 -368
- package/src/parser/client-ast.js +138 -0
- package/src/parser/client-parser.js +504 -0
- package/src/parser/parser.js +491 -1064
- package/src/parser/server-ast.js +240 -0
- package/src/parser/server-parser.js +602 -0
- package/src/runtime/array-proto.js +32 -0
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +191 -10
- package/src/stdlib/advanced-collections.js +81 -0
- package/src/stdlib/inline.js +549 -6
- package/src/version.js +1 -1
package/src/lexer/lexer.js
CHANGED
|
@@ -13,15 +13,12 @@ export class Lexer {
|
|
|
13
13
|
this.length = source.length;
|
|
14
14
|
this._depth = _depth;
|
|
15
15
|
|
|
16
|
-
// JSX context tracking
|
|
16
|
+
// JSX context tracking (consolidated state machine)
|
|
17
17
|
this._jsxStack = []; // stack of 'tag' or 'cfblock' entries
|
|
18
|
-
this.
|
|
19
|
-
this._jsxSelfClosing = false; // true when / seen
|
|
20
|
-
this._jsxClosingTag = false; // true when </ detected
|
|
18
|
+
this._jsxTagMode = null; // null | 'open' | 'close' — current tag parsing state
|
|
19
|
+
this._jsxSelfClosing = false; // true when / seen in opening tag (before >)
|
|
21
20
|
this._jsxExprDepth = 0; // brace depth for {expr} inside JSX
|
|
22
|
-
this.
|
|
23
|
-
this._cfParenDepth = 0; // () and [] nesting in control flow condition
|
|
24
|
-
this._cfBraceDepth = 0; // {} nesting for expression braces (key={...})
|
|
21
|
+
this._jsxCF = null; // null | { paren: 0, brace: 0 } — control flow state
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
error(message) {
|
|
@@ -92,6 +89,12 @@ export class Lexer {
|
|
|
92
89
|
}
|
|
93
90
|
|
|
94
91
|
tokenize() {
|
|
92
|
+
// Strip shebang line if present (e.g. #!/usr/bin/env tova)
|
|
93
|
+
if (this.pos === 0 && this.source[0] === '#' && this.source[1] === '!') {
|
|
94
|
+
while (this.pos < this.length && this.source[this.pos] !== '\n') this.advance();
|
|
95
|
+
if (this.pos < this.length) this.advance();
|
|
96
|
+
}
|
|
97
|
+
|
|
95
98
|
while (this.pos < this.length) {
|
|
96
99
|
this.scanToken();
|
|
97
100
|
}
|
|
@@ -102,16 +105,18 @@ export class Lexer {
|
|
|
102
105
|
scanToken() {
|
|
103
106
|
// In JSX children mode, scan raw text instead of normal tokens
|
|
104
107
|
if (this._jsxStack.length > 0 && this._jsxExprDepth === 0 &&
|
|
105
|
-
!this.
|
|
106
|
-
!this._jsxControlFlowPending) {
|
|
108
|
+
!this._jsxTagMode && !this._jsxCF) {
|
|
107
109
|
return this._scanInJSXChildren();
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
const ch = this.peek();
|
|
111
113
|
|
|
112
|
-
// Skip whitespace (not newlines)
|
|
114
|
+
// Skip whitespace (not newlines) — batch skip for consecutive whitespace
|
|
113
115
|
if (this.isWhitespace(ch)) {
|
|
114
116
|
this.advance();
|
|
117
|
+
while (this.pos < this.length && this.isWhitespace(this.source[this.pos])) {
|
|
118
|
+
this.advance();
|
|
119
|
+
}
|
|
115
120
|
return;
|
|
116
121
|
}
|
|
117
122
|
|
|
@@ -143,17 +148,15 @@ export class Lexer {
|
|
|
143
148
|
break;
|
|
144
149
|
}
|
|
145
150
|
}
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
TokenType.
|
|
151
|
-
TokenType.
|
|
152
|
-
TokenType.
|
|
153
|
-
TokenType.NOT, TokenType.BANG,
|
|
154
|
-
TokenType.PLUS_ASSIGN, TokenType.MINUS_ASSIGN, TokenType.STAR_ASSIGN, TokenType.SLASH_ASSIGN,
|
|
151
|
+
// Negative list: if previous token ends an expression (produces a value),
|
|
152
|
+
// then / is division. Otherwise, / starts a regex.
|
|
153
|
+
// This is simpler and more robust — new token types default to regex context.
|
|
154
|
+
const divisionContextTokens = [
|
|
155
|
+
TokenType.IDENTIFIER, TokenType.NUMBER, TokenType.STRING, TokenType.STRING_TEMPLATE,
|
|
156
|
+
TokenType.TRUE, TokenType.FALSE, TokenType.NIL,
|
|
157
|
+
TokenType.RPAREN, TokenType.RBRACKET, TokenType.RBRACE,
|
|
155
158
|
];
|
|
156
|
-
if (prev &&
|
|
159
|
+
if (prev && !divisionContextTokens.includes(prev.type)) {
|
|
157
160
|
this.scanRegex();
|
|
158
161
|
return;
|
|
159
162
|
}
|
|
@@ -172,6 +175,11 @@ export class Lexer {
|
|
|
172
175
|
|
|
173
176
|
// Strings
|
|
174
177
|
if (ch === '"') {
|
|
178
|
+
// Triple-quote multiline strings: """..."""
|
|
179
|
+
if (this.peek(1) === '"' && this.peek(2) === '"') {
|
|
180
|
+
this.scanTripleQuoteString();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
175
183
|
this.scanString();
|
|
176
184
|
return;
|
|
177
185
|
}
|
|
@@ -232,12 +240,13 @@ export class Lexer {
|
|
|
232
240
|
return;
|
|
233
241
|
}
|
|
234
242
|
if (ch === '<') {
|
|
235
|
-
// In JSX children, set
|
|
243
|
+
// In JSX children, set tag mode directly (heuristic may fail after STRING tokens)
|
|
236
244
|
const nextCh = this.peek(1);
|
|
237
245
|
if (nextCh === '/') {
|
|
238
|
-
this.
|
|
239
|
-
} else if (this.isAlpha(nextCh)) {
|
|
240
|
-
|
|
246
|
+
this._jsxTagMode = 'close';
|
|
247
|
+
} else if (nextCh === '>' || this.isAlpha(nextCh)) {
|
|
248
|
+
// <tag or <> (fragment)
|
|
249
|
+
this._jsxTagMode = 'open';
|
|
241
250
|
}
|
|
242
251
|
this.scanOperator();
|
|
243
252
|
return;
|
|
@@ -253,10 +262,8 @@ export class Lexer {
|
|
|
253
262
|
}
|
|
254
263
|
if (['if', 'for', 'elif', 'else'].includes(word)) {
|
|
255
264
|
this.scanIdentifier();
|
|
256
|
-
// After keyword, enter control flow
|
|
257
|
-
this.
|
|
258
|
-
this._cfParenDepth = 0;
|
|
259
|
-
this._cfBraceDepth = 0;
|
|
265
|
+
// After keyword, enter control flow mode for normal scanning
|
|
266
|
+
this._jsxCF = { paren: 0, brace: 0 };
|
|
260
267
|
return;
|
|
261
268
|
}
|
|
262
269
|
}
|
|
@@ -453,31 +460,32 @@ export class Lexer {
|
|
|
453
460
|
const exprStartLine = this.line - 1; // 0-based offset for sub-lexer
|
|
454
461
|
const exprStartCol = this.column - 1;
|
|
455
462
|
let depth = 1;
|
|
456
|
-
|
|
463
|
+
// Use array-based building to avoid O(n^2) string concatenation
|
|
464
|
+
const exprParts = [];
|
|
457
465
|
while (this.pos < this.length && depth > 0) {
|
|
458
466
|
const ch = this.peek();
|
|
459
467
|
// Skip over string literals so braces inside them don't affect depth
|
|
460
468
|
if (ch === '"' || ch === "'" || ch === '`') {
|
|
461
469
|
const quote = ch;
|
|
462
|
-
|
|
470
|
+
exprParts.push(this.advance()); // opening quote
|
|
463
471
|
let strDepth = 0; // track interpolation depth inside nested strings
|
|
464
472
|
while (this.pos < this.length) {
|
|
465
473
|
if (this.peek() === '\\') {
|
|
466
|
-
|
|
467
|
-
if (this.pos < this.length)
|
|
474
|
+
exprParts.push(this.advance()); // backslash
|
|
475
|
+
if (this.pos < this.length) exprParts.push(this.advance()); // escaped char
|
|
468
476
|
} else if (quote === '"' && this.peek() === '{') {
|
|
469
477
|
strDepth++;
|
|
470
|
-
|
|
478
|
+
exprParts.push(this.advance());
|
|
471
479
|
} else if (quote === '"' && this.peek() === '}' && strDepth > 0) {
|
|
472
480
|
strDepth--;
|
|
473
|
-
|
|
481
|
+
exprParts.push(this.advance());
|
|
474
482
|
} else if (this.peek() === quote && strDepth === 0) {
|
|
475
483
|
break;
|
|
476
484
|
} else {
|
|
477
|
-
|
|
485
|
+
exprParts.push(this.advance());
|
|
478
486
|
}
|
|
479
487
|
}
|
|
480
|
-
if (this.pos < this.length)
|
|
488
|
+
if (this.pos < this.length) exprParts.push(this.advance()); // closing quote
|
|
481
489
|
continue;
|
|
482
490
|
}
|
|
483
491
|
if (ch === '{') depth++;
|
|
@@ -485,8 +493,9 @@ export class Lexer {
|
|
|
485
493
|
depth--;
|
|
486
494
|
if (depth === 0) break;
|
|
487
495
|
}
|
|
488
|
-
|
|
496
|
+
exprParts.push(this.advance());
|
|
489
497
|
}
|
|
498
|
+
const exprSource = exprParts.join('');
|
|
490
499
|
|
|
491
500
|
if (this.peek() !== '}') {
|
|
492
501
|
this.error('Unterminated string interpolation');
|
|
@@ -525,6 +534,218 @@ export class Lexer {
|
|
|
525
534
|
}
|
|
526
535
|
}
|
|
527
536
|
|
|
537
|
+
scanTripleQuoteString() {
|
|
538
|
+
const startLine = this.line;
|
|
539
|
+
const startCol = this.column;
|
|
540
|
+
this.advance(); // first "
|
|
541
|
+
this.advance(); // second "
|
|
542
|
+
this.advance(); // third "
|
|
543
|
+
|
|
544
|
+
// Skip a leading newline immediately after opening """
|
|
545
|
+
if (this.pos < this.length && this.peek() === '\n') {
|
|
546
|
+
this.advance();
|
|
547
|
+
} else if (this.pos < this.length && this.peek() === '\r' && this.peek(1) === '\n') {
|
|
548
|
+
this.advance();
|
|
549
|
+
this.advance();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const parts = [];
|
|
553
|
+
let current = '';
|
|
554
|
+
|
|
555
|
+
while (this.pos < this.length) {
|
|
556
|
+
// Check for closing """
|
|
557
|
+
if (this.peek() === '"' && this.peek(1) === '"' && this.peek(2) === '"') {
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Escape sequences (same as regular strings)
|
|
562
|
+
if (this.peek() === '\\') {
|
|
563
|
+
this.advance();
|
|
564
|
+
if (this.pos >= this.length) {
|
|
565
|
+
this.error('Unterminated multiline string');
|
|
566
|
+
}
|
|
567
|
+
const esc = this.advance();
|
|
568
|
+
switch (esc) {
|
|
569
|
+
case 'n': current += '\n'; break;
|
|
570
|
+
case 't': current += '\t'; break;
|
|
571
|
+
case 'r': current += '\r'; break;
|
|
572
|
+
case '\\': current += '\\'; break;
|
|
573
|
+
case '"': current += '"'; break;
|
|
574
|
+
case '{': current += '{'; break;
|
|
575
|
+
case '}': current += '}'; break;
|
|
576
|
+
default: current += '\\' + esc;
|
|
577
|
+
}
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// String interpolation: {expr}
|
|
582
|
+
if (this.peek() === '{') {
|
|
583
|
+
this.advance(); // {
|
|
584
|
+
if (current.length > 0) {
|
|
585
|
+
parts.push({ type: 'text', value: current });
|
|
586
|
+
current = '';
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const exprStartLine = this.line - 1;
|
|
590
|
+
const exprStartCol = this.column - 1;
|
|
591
|
+
let depth = 1;
|
|
592
|
+
let exprSource = '';
|
|
593
|
+
while (this.pos < this.length && depth > 0) {
|
|
594
|
+
const ch = this.peek();
|
|
595
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
596
|
+
const quote = ch;
|
|
597
|
+
exprSource += this.advance();
|
|
598
|
+
let strDepth = 0;
|
|
599
|
+
while (this.pos < this.length) {
|
|
600
|
+
if (this.peek() === '\\') {
|
|
601
|
+
exprSource += this.advance();
|
|
602
|
+
if (this.pos < this.length) exprSource += this.advance();
|
|
603
|
+
} else if (quote === '"' && this.peek() === '{') {
|
|
604
|
+
strDepth++;
|
|
605
|
+
exprSource += this.advance();
|
|
606
|
+
} else if (quote === '"' && this.peek() === '}' && strDepth > 0) {
|
|
607
|
+
strDepth--;
|
|
608
|
+
exprSource += this.advance();
|
|
609
|
+
} else if (this.peek() === quote && strDepth === 0) {
|
|
610
|
+
break;
|
|
611
|
+
} else {
|
|
612
|
+
exprSource += this.advance();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (this.pos < this.length) exprSource += this.advance();
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
if (ch === '{') depth++;
|
|
619
|
+
if (ch === '}') {
|
|
620
|
+
depth--;
|
|
621
|
+
if (depth === 0) break;
|
|
622
|
+
}
|
|
623
|
+
exprSource += this.advance();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (this.peek() !== '}') {
|
|
627
|
+
this.error('Unterminated string interpolation in multiline string');
|
|
628
|
+
}
|
|
629
|
+
this.advance(); // }
|
|
630
|
+
|
|
631
|
+
if (this._depth + 1 > Lexer.MAX_INTERPOLATION_DEPTH) {
|
|
632
|
+
this.error('String interpolation nested too deeply (max ' + Lexer.MAX_INTERPOLATION_DEPTH + ' levels)');
|
|
633
|
+
}
|
|
634
|
+
const subLexer = new Lexer(exprSource, this.filename, exprStartLine, exprStartCol, this._depth + 1);
|
|
635
|
+
const exprTokens = subLexer.tokenize();
|
|
636
|
+
exprTokens.pop();
|
|
637
|
+
|
|
638
|
+
parts.push({ type: 'expr', tokens: exprTokens, source: exprSource });
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
current += this.advance();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (this.pos >= this.length || this.peek() !== '"') {
|
|
646
|
+
this.error('Unterminated multiline string (expected closing \"\"\")')
|
|
647
|
+
}
|
|
648
|
+
this.advance(); // first closing "
|
|
649
|
+
this.advance(); // second closing "
|
|
650
|
+
this.advance(); // third closing "
|
|
651
|
+
|
|
652
|
+
// Auto-dedent: find the indentation of the closing """ line
|
|
653
|
+
// Look back in `current` for the last newline to get the indentation
|
|
654
|
+
let rawContent = current;
|
|
655
|
+
if (parts.length === 0) {
|
|
656
|
+
// Simple string, no interpolation — auto-dedent
|
|
657
|
+
const dedented = this._dedentTripleQuote(rawContent);
|
|
658
|
+
this.tokens.push(new Token(TokenType.STRING, dedented, startLine, startCol));
|
|
659
|
+
} else {
|
|
660
|
+
// Template string — dedent text parts
|
|
661
|
+
if (current.length > 0) {
|
|
662
|
+
parts.push({ type: 'text', value: current });
|
|
663
|
+
}
|
|
664
|
+
const dedentedParts = this._dedentTripleQuoteParts(parts);
|
|
665
|
+
if (dedentedParts.length === 1 && dedentedParts[0].type === 'text') {
|
|
666
|
+
this.tokens.push(new Token(TokenType.STRING, dedentedParts[0].value, startLine, startCol));
|
|
667
|
+
} else {
|
|
668
|
+
this.tokens.push(new Token(TokenType.STRING_TEMPLATE, dedentedParts, startLine, startCol));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
_dedentTripleQuote(text) {
|
|
674
|
+
// Remove trailing whitespace-only line (the line before closing """)
|
|
675
|
+
if (text.endsWith('\n')) {
|
|
676
|
+
text = text.slice(0, -1);
|
|
677
|
+
} else {
|
|
678
|
+
// Check for trailing whitespace-only content after last newline
|
|
679
|
+
const lastNl = text.lastIndexOf('\n');
|
|
680
|
+
if (lastNl !== -1) {
|
|
681
|
+
const lastLine = text.slice(lastNl + 1);
|
|
682
|
+
if (lastLine.trim() === '') {
|
|
683
|
+
text = text.slice(0, lastNl);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const lines = text.split('\n');
|
|
689
|
+
// Find minimum indentation of non-empty lines
|
|
690
|
+
let minIndent = Infinity;
|
|
691
|
+
for (const line of lines) {
|
|
692
|
+
if (line.trim().length === 0) continue;
|
|
693
|
+
const indent = line.match(/^[ \t]*/)[0].length;
|
|
694
|
+
if (indent < minIndent) minIndent = indent;
|
|
695
|
+
}
|
|
696
|
+
if (minIndent === Infinity) minIndent = 0;
|
|
697
|
+
|
|
698
|
+
// Strip the common indentation
|
|
699
|
+
return lines.map(line => {
|
|
700
|
+
if (line.trim().length === 0) return '';
|
|
701
|
+
return line.slice(minIndent);
|
|
702
|
+
}).join('\n');
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
_dedentTripleQuoteParts(parts) {
|
|
706
|
+
// Collect all text to determine minimum indent
|
|
707
|
+
let allText = '';
|
|
708
|
+
for (const p of parts) {
|
|
709
|
+
if (p.type === 'text') allText += p.value;
|
|
710
|
+
else allText += 'X'; // placeholder for expressions
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Remove trailing whitespace-only line
|
|
714
|
+
if (allText.endsWith('\n')) {
|
|
715
|
+
// Trim trailing newline from last text part
|
|
716
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
717
|
+
if (parts[i].type === 'text' && parts[i].value.endsWith('\n')) {
|
|
718
|
+
parts[i] = { type: 'text', value: parts[i].value.slice(0, -1) };
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Find minimum indentation from text parts
|
|
725
|
+
let minIndent = Infinity;
|
|
726
|
+
for (const p of parts) {
|
|
727
|
+
if (p.type !== 'text') continue;
|
|
728
|
+
const lines = p.value.split('\n');
|
|
729
|
+
for (const line of lines) {
|
|
730
|
+
if (line.trim().length === 0) continue;
|
|
731
|
+
const indent = line.match(/^[ \t]*/)[0].length;
|
|
732
|
+
if (indent < minIndent) minIndent = indent;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (minIndent === Infinity || minIndent === 0) return parts;
|
|
736
|
+
|
|
737
|
+
// Dedent text parts
|
|
738
|
+
return parts.map(p => {
|
|
739
|
+
if (p.type !== 'text') return p;
|
|
740
|
+
const lines = p.value.split('\n');
|
|
741
|
+
const dedented = lines.map(line => {
|
|
742
|
+
if (line.trim().length === 0) return '';
|
|
743
|
+
return line.slice(Math.min(minIndent, line.match(/^[ \t]*/)[0].length));
|
|
744
|
+
}).join('\n');
|
|
745
|
+
return { type: 'text', value: dedented };
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
528
749
|
scanSimpleString() {
|
|
529
750
|
const startLine = this.line;
|
|
530
751
|
const startCol = this.column;
|
|
@@ -629,6 +850,13 @@ export class Lexer {
|
|
|
629
850
|
return;
|
|
630
851
|
}
|
|
631
852
|
|
|
853
|
+
// f-string: f"Hello, {name}!" — explicit interpolation sigil
|
|
854
|
+
// Delegates to scanString() which already handles interpolation
|
|
855
|
+
if (value === 'f' && this.pos < this.length && this.peek() === '"') {
|
|
856
|
+
this.scanString();
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
632
860
|
// Special case: "style {" → read raw CSS block
|
|
633
861
|
if (value === 'style') {
|
|
634
862
|
const savedPos = this.pos;
|
|
@@ -676,29 +904,29 @@ export class Lexer {
|
|
|
676
904
|
switch (ch) {
|
|
677
905
|
case '(':
|
|
678
906
|
this.tokens.push(new Token(TokenType.LPAREN, '(', startLine, startCol));
|
|
679
|
-
if (this.
|
|
907
|
+
if (this._jsxCF) this._jsxCF.paren++;
|
|
680
908
|
break;
|
|
681
909
|
case ')':
|
|
682
910
|
this.tokens.push(new Token(TokenType.RPAREN, ')', startLine, startCol));
|
|
683
|
-
if (this.
|
|
911
|
+
if (this._jsxCF && this._jsxCF.paren > 0) this._jsxCF.paren--;
|
|
684
912
|
break;
|
|
685
913
|
case '{':
|
|
686
914
|
this.tokens.push(new Token(TokenType.LBRACE, '{', startLine, startCol));
|
|
687
|
-
if (this.
|
|
688
|
-
if (this.
|
|
915
|
+
if (this._jsxCF) {
|
|
916
|
+
if (this._jsxCF.brace > 0) {
|
|
689
917
|
// Nested brace inside expression (e.g., key={obj.field})
|
|
690
|
-
this.
|
|
691
|
-
} else if (this.
|
|
918
|
+
this._jsxCF.brace++;
|
|
919
|
+
} else if (this._jsxCF.paren > 0) {
|
|
692
920
|
// Inside parens, this is an expression brace
|
|
693
|
-
this.
|
|
921
|
+
this._jsxCF.brace++;
|
|
694
922
|
} else {
|
|
695
923
|
// Check if prev token is ASSIGN (key={...}) or FOR (destructuring: for {a,b} in ...)
|
|
696
924
|
const prev = this.tokens.length > 1 ? this.tokens[this.tokens.length - 2] : null;
|
|
697
925
|
if (prev && (prev.type === TokenType.ASSIGN || prev.type === TokenType.FOR)) {
|
|
698
|
-
this.
|
|
926
|
+
this._jsxCF.brace++;
|
|
699
927
|
} else {
|
|
700
928
|
// This is the block opener for the control flow body
|
|
701
|
-
this.
|
|
929
|
+
this._jsxCF = null;
|
|
702
930
|
this._jsxStack.push('cfblock');
|
|
703
931
|
}
|
|
704
932
|
}
|
|
@@ -708,19 +936,19 @@ export class Lexer {
|
|
|
708
936
|
break;
|
|
709
937
|
case '}':
|
|
710
938
|
this.tokens.push(new Token(TokenType.RBRACE, '}', startLine, startCol));
|
|
711
|
-
if (this.
|
|
712
|
-
this.
|
|
939
|
+
if (this._jsxCF && this._jsxCF.brace > 0) {
|
|
940
|
+
this._jsxCF.brace--;
|
|
713
941
|
} else if (this._jsxExprDepth > 0) {
|
|
714
942
|
this._jsxExprDepth--;
|
|
715
943
|
}
|
|
716
944
|
break;
|
|
717
945
|
case '[':
|
|
718
946
|
this.tokens.push(new Token(TokenType.LBRACKET, '[', startLine, startCol));
|
|
719
|
-
if (this.
|
|
947
|
+
if (this._jsxCF) this._jsxCF.paren++;
|
|
720
948
|
break;
|
|
721
949
|
case ']':
|
|
722
950
|
this.tokens.push(new Token(TokenType.RBRACKET, ']', startLine, startCol));
|
|
723
|
-
if (this.
|
|
951
|
+
if (this._jsxCF && this._jsxCF.paren > 0) this._jsxCF.paren--;
|
|
724
952
|
break;
|
|
725
953
|
case ',':
|
|
726
954
|
this.tokens.push(new Token(TokenType.COMMA, ',', startLine, startCol));
|
|
@@ -762,7 +990,7 @@ export class Lexer {
|
|
|
762
990
|
this.tokens.push(new Token(TokenType.SLASH_ASSIGN, '/=', startLine, startCol));
|
|
763
991
|
} else {
|
|
764
992
|
this.tokens.push(new Token(TokenType.SLASH, '/', startLine, startCol));
|
|
765
|
-
if (this.
|
|
993
|
+
if (this._jsxTagMode === 'open') this._jsxSelfClosing = true;
|
|
766
994
|
}
|
|
767
995
|
break;
|
|
768
996
|
|
|
@@ -793,12 +1021,26 @@ export class Lexer {
|
|
|
793
1021
|
this.tokens.push(new Token(TokenType.LESS_EQUAL, '<=', startLine, startCol));
|
|
794
1022
|
} else {
|
|
795
1023
|
this.tokens.push(new Token(TokenType.LESS, '<', startLine, startCol));
|
|
796
|
-
// Don't override
|
|
797
|
-
if (!this.
|
|
1024
|
+
// Don't override tag mode already set by _scanInJSXChildren
|
|
1025
|
+
if (!this._jsxTagMode) {
|
|
798
1026
|
if (this.peek() === '/') {
|
|
799
|
-
this.
|
|
1027
|
+
this._jsxTagMode = 'close';
|
|
1028
|
+
} else if (this.peek() === '>') {
|
|
1029
|
+
// Fragment: <> — use same heuristic as _isJSXStart
|
|
1030
|
+
// to avoid false positives in expressions
|
|
1031
|
+
if (this._jsxStack.length > 0) {
|
|
1032
|
+
this._jsxTagMode = 'open';
|
|
1033
|
+
} else {
|
|
1034
|
+
const prev = this.tokens.length > 1 ? this.tokens[this.tokens.length - 2] : null;
|
|
1035
|
+
const valueTypes = [TokenType.IDENTIFIER, TokenType.NUMBER, TokenType.STRING,
|
|
1036
|
+
TokenType.STRING_TEMPLATE, TokenType.RPAREN, TokenType.RBRACKET, TokenType.RBRACE,
|
|
1037
|
+
TokenType.TRUE, TokenType.FALSE, TokenType.NIL];
|
|
1038
|
+
if (!prev || !valueTypes.includes(prev.type)) {
|
|
1039
|
+
this._jsxTagMode = 'open';
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
800
1042
|
} else if (this._isJSXStart()) {
|
|
801
|
-
this.
|
|
1043
|
+
this._jsxTagMode = 'open';
|
|
802
1044
|
}
|
|
803
1045
|
}
|
|
804
1046
|
}
|
|
@@ -812,15 +1054,15 @@ export class Lexer {
|
|
|
812
1054
|
// JSX state transitions on >
|
|
813
1055
|
if (this._jsxSelfClosing) {
|
|
814
1056
|
// Self-closing tag: <br/> — don't push to stack
|
|
815
|
-
this.
|
|
1057
|
+
this._jsxTagMode = null;
|
|
816
1058
|
this._jsxSelfClosing = false;
|
|
817
|
-
} else if (this.
|
|
818
|
-
// Closing tag: </div> — pop 'tag' from stack
|
|
819
|
-
this.
|
|
1059
|
+
} else if (this._jsxTagMode === 'close') {
|
|
1060
|
+
// Closing tag: </div> or </> — pop 'tag' from stack
|
|
1061
|
+
this._jsxTagMode = null;
|
|
820
1062
|
if (this._jsxStack.length > 0) this._jsxStack.pop();
|
|
821
|
-
} else if (this.
|
|
822
|
-
// Opening tag: <div> — push 'tag' to stack (entering children mode)
|
|
823
|
-
this.
|
|
1063
|
+
} else if (this._jsxTagMode === 'open') {
|
|
1064
|
+
// Opening tag: <div> or <> — push 'tag' to stack (entering children mode)
|
|
1065
|
+
this._jsxTagMode = null;
|
|
824
1066
|
this._jsxStack.push('tag');
|
|
825
1067
|
}
|
|
826
1068
|
}
|
|
@@ -844,7 +1086,7 @@ export class Lexer {
|
|
|
844
1086
|
} else if (this.match('|')) {
|
|
845
1087
|
this.tokens.push(new Token(TokenType.OR_OR, '||', startLine, startCol));
|
|
846
1088
|
} else {
|
|
847
|
-
this.
|
|
1089
|
+
this.tokens.push(new Token(TokenType.BAR, '|', startLine, startCol));
|
|
848
1090
|
}
|
|
849
1091
|
break;
|
|
850
1092
|
|
package/src/lexer/tokens.js
CHANGED
|
@@ -64,12 +64,25 @@ export const TokenType = {
|
|
|
64
64
|
// Defer
|
|
65
65
|
DEFER: 'DEFER',
|
|
66
66
|
|
|
67
|
+
// Mutable (alias for var)
|
|
68
|
+
MUT: 'MUT',
|
|
69
|
+
|
|
70
|
+
// Loop
|
|
71
|
+
LOOP: 'LOOP',
|
|
72
|
+
WHEN: 'WHEN',
|
|
73
|
+
|
|
67
74
|
// Generators
|
|
68
75
|
YIELD: 'YIELD',
|
|
69
76
|
|
|
70
77
|
// Extern
|
|
71
78
|
EXTERN: 'EXTERN',
|
|
72
79
|
|
|
80
|
+
// Type checking
|
|
81
|
+
IS: 'IS',
|
|
82
|
+
|
|
83
|
+
// Resource management
|
|
84
|
+
WITH: 'WITH',
|
|
85
|
+
|
|
73
86
|
// Full-stack keywords
|
|
74
87
|
SERVER: 'SERVER',
|
|
75
88
|
CLIENT: 'CLIENT',
|
|
@@ -107,6 +120,7 @@ export const TokenType = {
|
|
|
107
120
|
OR_OR: 'OR_OR', // ||
|
|
108
121
|
BANG: 'BANG', // !
|
|
109
122
|
PIPE: 'PIPE', // |>
|
|
123
|
+
BAR: 'BAR', // |
|
|
110
124
|
ARROW: 'ARROW', // =>
|
|
111
125
|
THIN_ARROW: 'THIN_ARROW', // ->
|
|
112
126
|
DOT: 'DOT', // .
|
|
@@ -186,8 +200,13 @@ export const Keywords = {
|
|
|
186
200
|
'impl': TokenType.IMPL,
|
|
187
201
|
'trait': TokenType.TRAIT,
|
|
188
202
|
'defer': TokenType.DEFER,
|
|
203
|
+
'mut': TokenType.MUT,
|
|
189
204
|
'yield': TokenType.YIELD,
|
|
205
|
+
'loop': TokenType.LOOP,
|
|
206
|
+
'when': TokenType.WHEN,
|
|
190
207
|
'extern': TokenType.EXTERN,
|
|
208
|
+
'is': TokenType.IS,
|
|
209
|
+
'with': TokenType.WITH,
|
|
191
210
|
'server': TokenType.SERVER,
|
|
192
211
|
'client': TokenType.CLIENT,
|
|
193
212
|
'shared': TokenType.SHARED,
|