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.
@@ -13,15 +13,12 @@ export class Lexer {
13
13
  this.length = source.length;
14
14
  this._depth = _depth;
15
15
 
16
- // JSX context tracking for unquoted text support
16
+ // JSX context tracking (consolidated state machine)
17
17
  this._jsxStack = []; // stack of 'tag' or 'cfblock' entries
18
- this._jsxTagOpening = false; // true when < starts a JSX opening tag
19
- this._jsxSelfClosing = false; // true when / seen inside JSX tag (before >)
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._jsxControlFlowPending = false; // true after if/for/elif/else keyword in JSX
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._jsxTagOpening && !this._jsxClosingTag &&
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
- // Only treat as regex after tokens that clearly start an expression context
147
- const regexPreceders = [
148
- TokenType.ASSIGN, TokenType.LPAREN, TokenType.LBRACKET, TokenType.LBRACE,
149
- TokenType.COMMA, TokenType.COLON, TokenType.SEMICOLON,
150
- TokenType.RETURN, TokenType.ARROW, TokenType.PIPE,
151
- TokenType.EQUAL, TokenType.NOT_EQUAL,
152
- TokenType.AND, TokenType.OR, TokenType.AND_AND, TokenType.OR_OR,
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 && regexPreceders.includes(prev.type)) {
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 flags directly (heuristic may fail after STRING tokens)
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._jsxClosingTag = true;
239
- } else if (this.isAlpha(nextCh)) {
240
- this._jsxTagOpening = true;
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 pending mode for normal scanning
257
- this._jsxControlFlowPending = true;
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
- let exprSource = '';
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
- exprSource += this.advance(); // opening quote
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
- exprSource += this.advance(); // backslash
467
- if (this.pos < this.length) exprSource += this.advance(); // escaped char
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
- exprSource += this.advance();
478
+ exprParts.push(this.advance());
471
479
  } else if (quote === '"' && this.peek() === '}' && strDepth > 0) {
472
480
  strDepth--;
473
- exprSource += this.advance();
481
+ exprParts.push(this.advance());
474
482
  } else if (this.peek() === quote && strDepth === 0) {
475
483
  break;
476
484
  } else {
477
- exprSource += this.advance();
485
+ exprParts.push(this.advance());
478
486
  }
479
487
  }
480
- if (this.pos < this.length) exprSource += this.advance(); // closing quote
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
- exprSource += this.advance();
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._jsxControlFlowPending) this._cfParenDepth++;
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._jsxControlFlowPending && this._cfParenDepth > 0) this._cfParenDepth--;
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._jsxControlFlowPending) {
688
- if (this._cfBraceDepth > 0) {
915
+ if (this._jsxCF) {
916
+ if (this._jsxCF.brace > 0) {
689
917
  // Nested brace inside expression (e.g., key={obj.field})
690
- this._cfBraceDepth++;
691
- } else if (this._cfParenDepth > 0) {
918
+ this._jsxCF.brace++;
919
+ } else if (this._jsxCF.paren > 0) {
692
920
  // Inside parens, this is an expression brace
693
- this._cfBraceDepth++;
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._cfBraceDepth++;
926
+ this._jsxCF.brace++;
699
927
  } else {
700
928
  // This is the block opener for the control flow body
701
- this._jsxControlFlowPending = false;
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._jsxControlFlowPending && this._cfBraceDepth > 0) {
712
- this._cfBraceDepth--;
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._jsxControlFlowPending) this._cfParenDepth++;
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._jsxControlFlowPending && this._cfParenDepth > 0) this._cfParenDepth--;
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._jsxTagOpening) this._jsxSelfClosing = true;
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 flags already set by _scanInJSXChildren
797
- if (!this._jsxClosingTag && !this._jsxTagOpening) {
1024
+ // Don't override tag mode already set by _scanInJSXChildren
1025
+ if (!this._jsxTagMode) {
798
1026
  if (this.peek() === '/') {
799
- this._jsxClosingTag = true;
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._jsxTagOpening = true;
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._jsxTagOpening = false;
1057
+ this._jsxTagMode = null;
816
1058
  this._jsxSelfClosing = false;
817
- } else if (this._jsxClosingTag) {
818
- // Closing tag: </div> — pop 'tag' from stack
819
- this._jsxClosingTag = false;
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._jsxTagOpening) {
822
- // Opening tag: <div> — push 'tag' to stack (entering children mode)
823
- this._jsxTagOpening = false;
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.error(`Unexpected character: '|'. Did you mean '|>' or '||'?`);
1089
+ this.tokens.push(new Token(TokenType.BAR, '|', startLine, startCol));
848
1090
  }
849
1091
  break;
850
1092
 
@@ -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,