tova 0.3.4 → 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.
@@ -1,37 +1,29 @@
1
1
  import { TokenType } from '../lexer/tokens.js';
2
2
  import * as AST from './ast.js';
3
+ import { installServerParser } from './server-parser.js';
4
+ import { installClientParser } from './client-parser.js';
3
5
 
4
6
  export class Parser {
5
7
  static MAX_EXPRESSION_DEPTH = 200;
8
+ static COMPARISON_OPS = null; // initialized after class definition
6
9
 
7
10
  constructor(tokens, filename = '<stdin>') {
8
- this.tokens = tokens;
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
9
22
  this.filename = filename;
10
23
  this.pos = 0;
11
24
  this.errors = [];
12
25
  this._expressionDepth = 0;
13
- this.docstrings = this.extractDocstrings(tokens);
14
- this._skipInsignificant();
15
- }
16
-
17
- _isInsignificant(type) {
18
- return type === TokenType.NEWLINE || type === TokenType.DOCSTRING || type === TokenType.SEMICOLON;
19
- }
20
-
21
- _skipInsignificant() {
22
- while (this.pos < this.tokens.length && this._isInsignificant(this.tokens[this.pos].type)) {
23
- this.pos++;
24
- }
25
- }
26
-
27
- extractDocstrings(tokens) {
28
- const docs = [];
29
- for (const t of tokens) {
30
- if (t.type === TokenType.DOCSTRING) {
31
- docs.push(t);
32
- }
33
- }
34
- return docs;
26
+ this.docstrings = docs;
35
27
  }
36
28
 
37
29
  // ─── Helpers ───────────────────────────────────────────────
@@ -47,28 +39,16 @@ export class Parser {
47
39
  }
48
40
 
49
41
  current() {
50
- return this.tokens[this.pos] || this.tokens[this.tokens.length - 1];
42
+ return this.tokens[this.pos] || this._eof;
51
43
  }
52
44
 
53
45
  peek(offset = 0) {
54
- // Fast path: offset 0 is just the current position (always significant after skip)
55
- if (offset === 0) return this.tokens[this.pos] || this.tokens[this.tokens.length - 1];
56
- // General path: skip over insignificant tokens
57
- let count = 0;
58
- for (let idx = this.pos + 1; idx < this.tokens.length; idx++) {
59
- if (!this._isInsignificant(this.tokens[idx].type)) {
60
- count++;
61
- if (count === offset) return this.tokens[idx];
62
- }
63
- }
64
- return this.tokens[this.tokens.length - 1];
46
+ const idx = this.pos + offset;
47
+ return idx < this.tokens.length ? this.tokens[idx] : this._eof;
65
48
  }
66
49
 
67
50
  advance() {
68
- const tok = this.current();
69
- this.pos++;
70
- this._skipInsignificant();
71
- return tok;
51
+ return this.tokens[this.pos++] || this._eof;
72
52
  }
73
53
 
74
54
  check(type) {
@@ -248,8 +228,8 @@ export class Parser {
248
228
  }
249
229
 
250
230
  _attachDocstrings(program) {
251
- // Build a map of docstring line ranges from raw tokens
252
- const docTokens = this.tokens.filter(t => t.type === TokenType.DOCSTRING);
231
+ // Use pre-extracted docstring tokens
232
+ const docTokens = this.docstrings;
253
233
  if (docTokens.length === 0) return;
254
234
 
255
235
  // Group consecutive docstring lines
@@ -295,14 +275,12 @@ export class Parser {
295
275
  parseTopLevel() {
296
276
  if (this.check(TokenType.SERVER)) {
297
277
  if (!Parser.prototype._serverParserInstalled) {
298
- const { installServerParser } = import.meta.require('./server-parser.js');
299
278
  installServerParser(Parser);
300
279
  }
301
280
  return this.parseServerBlock();
302
281
  }
303
282
  if (this.check(TokenType.CLIENT)) {
304
283
  if (!Parser.prototype._clientParserInstalled) {
305
- const { installClientParser } = import.meta.require('./client-parser.js');
306
284
  installClientParser(Parser);
307
285
  }
308
286
  return this.parseClientBlock();
@@ -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
- parseFunctionDeclaration() {
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 (++this._expressionDepth > Parser.MAX_EXPRESSION_DEPTH) {
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
- const compOps = [TokenType.LESS, TokenType.LESS_EQUAL, TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.EQUAL, TokenType.NOT_EQUAL];
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 (true) {
1593
- const op = this.match(...compOps);
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
- // Number
1890
- if (this.check(TokenType.NUMBER)) {
1891
- return new AST.NumberLiteral(this.advance().value, l);
1892
- }
1894
+ switch (tokenType) {
1895
+ case TokenType.NUMBER:
1896
+ return new AST.NumberLiteral(this.advance().value, l);
1893
1897
 
1894
- // String
1895
- if (this.check(TokenType.STRING) || this.check(TokenType.STRING_TEMPLATE)) {
1896
- return this.parseStringLiteral();
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
- // Boolean
1906
- if (this.check(TokenType.TRUE)) {
1907
- this.advance();
1908
- return new AST.BooleanLiteral(true, l);
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
- // Nil
1916
- if (this.check(TokenType.NIL)) {
1917
- this.advance();
1918
- return new AST.NilLiteral(l);
1919
- }
1907
+ case TokenType.TRUE:
1908
+ this.advance();
1909
+ return new AST.BooleanLiteral(true, l);
1920
1910
 
1921
- // Match expression
1922
- if (this.check(TokenType.MATCH)) {
1923
- return this.parseMatchExpression();
1924
- }
1911
+ case TokenType.FALSE:
1912
+ this.advance();
1913
+ return new AST.BooleanLiteral(false, l);
1925
1914
 
1926
- // If expression (in expression position): if cond { a } else { b }
1927
- if (this.check(TokenType.IF)) {
1928
- return this.parseIfExpression();
1929
- }
1915
+ case TokenType.NIL:
1916
+ this.advance();
1917
+ return new AST.NilLiteral(l);
1930
1918
 
1931
- // Async lambda: async fn(params) body
1932
- if (this.check(TokenType.ASYNC) && this.peek(1).type === TokenType.FN) {
1933
- return this.parseAsyncLambda();
1934
- }
1919
+ case TokenType.MATCH:
1920
+ return this.parseMatchExpression();
1935
1921
 
1936
- // Lambda: fn(params) body or params => body
1937
- if (this.check(TokenType.FN) && this.peek(1).type === TokenType.LPAREN) {
1938
- return this.parseLambda();
1939
- }
1922
+ case TokenType.IF:
1923
+ return this.parseIfExpression();
1940
1924
 
1941
- // Arrow lambda: x => expr or (x, y) => expr
1942
- // We'll handle this in the identifier/paren case
1925
+ case TokenType.ASYNC:
1926
+ if (this.peek(1).type === TokenType.FN) {
1927
+ return this.parseAsyncLambda();
1928
+ }
1929
+ break;
1943
1930
 
1944
- // Array literal or list comprehension
1945
- if (this.check(TokenType.LBRACKET)) {
1946
- return this.parseArrayOrComprehension();
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
- // Object literal or dict comprehension
1950
- if (this.check(TokenType.LBRACE)) {
1951
- return this.parseObjectOrDictComprehension();
1952
- }
1958
+ case TokenType.LPAREN:
1959
+ return this.parseParenOrArrowLambda();
1953
1960
 
1954
- // Column expression: .column (for table operations)
1955
- // Appears at expression-start when DOT followed by IDENTIFIER
1956
- // but NOT when preceded by an expression (which would be member access)
1957
- if (this.check(TokenType.DOT) && this.peek(1).type === TokenType.IDENTIFIER) {
1958
- // Check this isn't inside a method pipe (|> .method) — that's handled in parsePipe
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
- // Check for column assignment: .col = expr (used in derive)
1964
- if (this.check(TokenType.ASSIGN)) {
1965
- this.advance(); // consume =
1966
- const expr = this.parseExpression();
1967
- return new AST.ColumnAssignment(name, expr, l);
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
- // Identifier (or arrow lambda: x => expr)
1986
- if (this.check(TokenType.IDENTIFIER)) {
1987
- const name = this.advance().value;
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
+ ]);