@pineforge/codegen-pyodide 0.10.4 → 1.0.0-rc.1

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.
Files changed (50) hide show
  1. package/README.md +16 -16
  2. package/glue.py +24 -16
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +125 -34
  5. package/pineforge_codegen/analyzer/__init__.py +2 -0
  6. package/pineforge_codegen/analyzer/base.py +754 -76
  7. package/pineforge_codegen/analyzer/call_handlers.py +260 -40
  8. package/pineforge_codegen/analyzer/contracts.py +37 -0
  9. package/pineforge_codegen/analyzer/diagnostics.py +30 -4
  10. package/pineforge_codegen/analyzer/tables.py +49 -8
  11. package/pineforge_codegen/analyzer/types.py +33 -1
  12. package/pineforge_codegen/ast_nodes.py +32 -1
  13. package/pineforge_codegen/block_locals.py +185 -0
  14. package/pineforge_codegen/builtin_keywords.py +42 -0
  15. package/pineforge_codegen/codegen/base.py +896 -156
  16. package/pineforge_codegen/codegen/constant_fold.py +131 -0
  17. package/pineforge_codegen/codegen/drawing.py +221 -79
  18. package/pineforge_codegen/codegen/emit_top.py +946 -213
  19. package/pineforge_codegen/codegen/helpers.py +435 -14
  20. package/pineforge_codegen/codegen/host_members.py +162 -0
  21. package/pineforge_codegen/codegen/input.py +252 -85
  22. package/pineforge_codegen/codegen/security.py +4372 -377
  23. package/pineforge_codegen/codegen/session_market.py +71 -0
  24. package/pineforge_codegen/codegen/ta.py +1188 -100
  25. package/pineforge_codegen/codegen/tables.py +193 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +1882 -78
  28. package/pineforge_codegen/codegen/visit_call.py +920 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +738 -57
  30. package/pineforge_codegen/codegen/visit_stmt.py +595 -49
  31. package/pineforge_codegen/external_requests.py +877 -0
  32. package/pineforge_codegen/lexer.py +104 -22
  33. package/pineforge_codegen/library_inline.py +1304 -0
  34. package/pineforge_codegen/library_modules.py +126 -0
  35. package/pineforge_codegen/library_v5.py +683 -0
  36. package/pineforge_codegen/limits.py +138 -0
  37. package/pineforge_codegen/method_binding.py +33 -0
  38. package/pineforge_codegen/parser.py +384 -68
  39. package/pineforge_codegen/pine_libraries.py +266 -0
  40. package/pineforge_codegen/pine_spelling.py +216 -0
  41. package/pineforge_codegen/pragmas.py +64 -10
  42. package/pineforge_codegen/security_contexts.py +1585 -0
  43. package/pineforge_codegen/session_reads.py +84 -0
  44. package/pineforge_codegen/signatures.py +48 -23
  45. package/pineforge_codegen/support_checker.py +1106 -85
  46. package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
  47. package/release.json +2 -2
  48. package/tables.json +23 -21
  49. package/transpile.worker.mjs +24 -16
  50. package/pineforge_codegen-0.10.4.tar.gz +0 -0
@@ -11,18 +11,20 @@ Rewritten parser (Tasks 5 & 6) that:
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ import math
14
15
  import re
15
16
 
16
17
  from .lexer import Token, TokenType
17
18
  from .errors import CompileError, Diagnostic, Level, Phase, SourceLocation
19
+ from .limits import MAX_NESTING_DEPTH, TimeBudget, limit_error, syntax_children
18
20
  from .ast_nodes import (
19
- ASTNode,
21
+ ASTNode, ArgOrder,
20
22
  Program, StrategyDecl, ImportStmt,
21
23
  VarDecl, Assignment, TupleAssign,
22
24
  IfStmt, ForStmt, ForInStmt, WhileStmt, SwitchStmt, BreakStmt, ContinueStmt,
23
25
  FuncDef, ExprStmt,
24
26
  BinOp, UnaryOp, Ternary, FuncCall, Subscript,
25
- Identifier, MemberAccess, TypeAnnotation,
27
+ Identifier, MemberAccess,
26
28
  NumberLiteral, StringLiteral, BoolLiteral, NaLiteral, ColorLiteral,
27
29
  TupleLiteral,
28
30
  TypeField, TypeDecl, EnumDecl, MethodDef,
@@ -30,9 +32,20 @@ from .ast_nodes import (
30
32
 
31
33
 
32
34
  class ParseError(Exception):
33
- pass
35
+ def __init__(self, message: str, token: Token) -> None:
36
+ super().__init__(message)
37
+ self.token = token
34
38
 
35
39
 
40
+ # ``//@version=N`` as TradingView reads it (tests/test_version_directive.py): the
41
+ # whole line, spaces, tabs or form feeds allowed before ``//``, between ``//``
42
+ # and ``@version``, around ``=`` and at the end, ASCII digits; a line ends at
43
+ # ``\r\n``, ``\r`` or ``\n``.
44
+ _VERSION_DIRECTIVE_RE = re.compile(
45
+ r"[ \t\f]*//[ \t\f]*@version[ \t\f]*=[ \t\f]*([0-9]+)[ \t\f]*"
46
+ )
47
+ _TRADINGVIEW_LINE_END_RE = re.compile(r"\r\n|\r|\n")
48
+
36
49
  # Type annotation keywords
37
50
  TYPE_KEYWORDS = {
38
51
  TokenType.TYPE_INT, TokenType.TYPE_FLOAT,
@@ -51,12 +64,22 @@ COMPOUND_ASSIGN_OPS = {
51
64
 
52
65
 
53
66
  class Parser:
54
- def __init__(self, tokens: list[Token], *, source: str = "", filename: str = "<input>") -> None:
67
+ def __init__(self, tokens: list[Token], *, source: str = "", filename: str = "<input>",
68
+ budget: TimeBudget | None = None, library: bool = False) -> None:
55
69
  self.tokens = tokens
56
70
  self.pos = 0
57
71
  self._source = source
58
72
  self._filename = filename
59
- self._recovery_count = 0
73
+ self._budget = budget
74
+ # A Pine library module (``library_modules``): its ``library()``
75
+ # declaration and ``export`` declarations parse instead of being
76
+ # refused, and a ``const`` qualifier is recorded on its declaration.
77
+ self._library = library
78
+ # Syntax levels around the parse position, and the depth of each
79
+ # operator/postfix chain subtree measured so far (held by id; the
80
+ # parser never discards a node it built, so ids stay unique).
81
+ self._depth = 0
82
+ self._subtree_depths: dict[int, tuple[ASTNode, int]] = {}
60
83
 
61
84
  # ------------------------------------------------------------------
62
85
  # Helpers
@@ -87,6 +110,8 @@ class Parser:
87
110
  def _advance(self) -> Token:
88
111
  tok = self._current()
89
112
  self.pos += 1
113
+ if self._budget is not None and self.pos % 128 == 0:
114
+ self._budget.check(self._loc(tok), Phase.PARSER)
90
115
  return tok
91
116
 
92
117
  def _consume(self, tt: TokenType, msg: str = "") -> Token:
@@ -94,8 +119,8 @@ class Parser:
94
119
  return self._advance()
95
120
  cur = self._current()
96
121
  raise ParseError(
97
- f"Expected {tt.name} got {cur.type.name}({cur.value!r}) "
98
- f"L{cur.line}:{cur.col}. {msg}"
122
+ f"Expected {tt.name}, got {cur.type.name}({cur.value!r}). {msg}".strip(),
123
+ cur,
99
124
  )
100
125
 
101
126
  def _skip_newlines(self) -> None:
@@ -121,6 +146,52 @@ class Parser:
121
146
  node.loc = self._loc(tok)
122
147
  return node
123
148
 
149
+ # Nesting budget. Recursive descent spends Python frames per level, and
150
+ # the tree it builds must stay shallow: freeing a tree some 4,000 levels
151
+ # deep overflows Pyodide's stack, fatally.
152
+
153
+ def _enter(self, tok: Token) -> None:
154
+ """Count one syntax level (a block, an ``else if``, a prefix operator
155
+ or a nested expression) before recursing into it."""
156
+ if self._depth >= MAX_NESTING_DEPTH:
157
+ raise limit_error(
158
+ f"Nesting depth exceeds {MAX_NESTING_DEPTH} levels.",
159
+ self._loc(tok), Phase.PARSER,
160
+ )
161
+ self._depth += 1
162
+
163
+ def _check_chain(self, node: ASTNode, tok: Token) -> ASTNode:
164
+ """Bound an operator or postfix chain, which grows without recursing."""
165
+ if self._subtree_depth(node) > MAX_NESTING_DEPTH:
166
+ raise limit_error(
167
+ f"AST nesting depth exceeds {MAX_NESTING_DEPTH} nodes.",
168
+ self._loc(tok), Phase.PARSER,
169
+ )
170
+ return node
171
+
172
+ def _subtree_depth(self, root: ASTNode) -> int:
173
+ """Depth of ``root``'s subtree, reusing the depths measured so far."""
174
+ known = self._subtree_depths
175
+ stack: list[tuple[ASTNode, bool]] = [(root, False)]
176
+ while stack:
177
+ node, expanded = stack.pop()
178
+ if id(node) in known:
179
+ continue
180
+ children = [
181
+ child for child in syntax_children(node)
182
+ if id(child) not in known
183
+ ]
184
+ if expanded or not children:
185
+ depth = 1 + max(
186
+ (known[id(child)][1] for child in syntax_children(node)),
187
+ default=0,
188
+ )
189
+ known[id(node)] = (node, depth)
190
+ else:
191
+ stack.append((node, True))
192
+ stack.extend((child, False) for child in children)
193
+ return known[id(root)][1]
194
+
124
195
  # ------------------------------------------------------------------
125
196
  # Top-level
126
197
  # ------------------------------------------------------------------
@@ -139,32 +210,51 @@ class Parser:
139
210
  prog.body.extend(stmt)
140
211
  else:
141
212
  prog.body.append(stmt)
142
- except ParseError:
143
- # Error recovery: skip to next newline and continue
144
- self._recover()
213
+ self._expect_statement_end()
214
+ except ParseError as error:
215
+ self._raise_syntax_error(error)
145
216
  self._skip_newlines()
146
217
 
147
- if self._recovery_count:
148
- prog.annotations = dict(prog.annotations or {})
149
- prog.annotations["parse_recovery_count"] = self._recovery_count
150
218
  return prog
151
219
 
152
220
  def _extract_version(self) -> int | None:
153
- """Extract version number from //@version=N annotation in source."""
221
+ """The version of the script's ``//@version=N`` directive.
222
+
223
+ TradingView's rule (tests/test_version_directive.py, 46 probes): the
224
+ FIRST line holding nothing but ``//``, ``@version``, ``=`` and ASCII
225
+ digits, with optional spaces, tabs or form feeds around each, anywhere
226
+ in the script -- read line by line, where a carriage return ends a line
227
+ too, so such a line inside a multiline string counts as well. Case,
228
+ ``///``, a space after ``@``, trailing text, code before it on the line,
229
+ any other blank (vertical tab, no-break space) or a carriage return
230
+ inside it make it no directive.
231
+ """
154
232
  if not self._source:
155
233
  return None
156
- m = re.search(r'//@version=(\d+)', self._source)
157
- if m:
158
- return int(m.group(1))
234
+ for line in _TRADINGVIEW_LINE_END_RE.split(self._source):
235
+ match = _VERSION_DIRECTIVE_RE.fullmatch(line)
236
+ if match is not None:
237
+ return int(match.group(1))
159
238
  return None
160
239
 
161
- def _recover(self) -> None:
162
- """Skip tokens until next NEWLINE or EOF for error recovery."""
163
- self._recovery_count += 1
164
- while not self._at_end() and not self._check(TokenType.NEWLINE):
165
- self._advance()
166
- if self._check(TokenType.NEWLINE):
167
- self._advance()
240
+ def _raise_syntax_error(self, error: ParseError) -> None:
241
+ raise CompileError([Diagnostic(
242
+ level=Level.ERROR, phase=Phase.PARSER,
243
+ location=self._loc(error.token), message=str(error),
244
+ )]) from error
245
+
246
+ def _expect_statement_end(self) -> None:
247
+ """A second expression on the same Pine line is a syntax error."""
248
+ cur = self._current()
249
+ if cur.type in (TokenType.NEWLINE, TokenType.DEDENT, TokenType.EOF_TOKEN):
250
+ return
251
+ previous = self.tokens[self.pos - 1]
252
+ if previous.type == TokenType.DEDENT or cur.line > previous.line:
253
+ return
254
+ raise ParseError(
255
+ f"Unexpected token {cur.type.name}({cur.value!r}) after statement; "
256
+ "expected a line break or comma", cur,
257
+ )
168
258
 
169
259
  # ------------------------------------------------------------------
170
260
  # Statement parsing
@@ -211,6 +301,18 @@ class Parser:
211
301
  tok = self._advance()
212
302
  return self._set_loc(ContinueStmt(), tok)
213
303
 
304
+ # ``export`` starts a declaration in Pine libraries, but no strategy
305
+ # may export a function. Refuse it at the authored keyword instead of
306
+ # parsing it as a standalone expression and reporting its function
307
+ # name as an unrelated trailing token.
308
+ if cur.type == TokenType.IDENT and cur.value == "export":
309
+ if self._library:
310
+ return self._parse_export_decl()
311
+ raise ParseError(
312
+ "'export' declarations belong to Pine libraries; "
313
+ "PineForge transpiles strategies only", cur,
314
+ )
315
+
214
316
  # import statement
215
317
  if cur.type == TokenType.IMPORT:
216
318
  return self._parse_import_stmt()
@@ -243,8 +345,20 @@ class Parser:
243
345
  and self._peek(3).type != TokenType.EQUALS
244
346
  )
245
347
  if typed_after_qual or bare_after_qual:
348
+ qualifier = cur.value
246
349
  self._advance() # consume the qualifier prefix
247
- return self._parse_single_statement()
350
+ stmt = self._parse_single_statement()
351
+ if isinstance(stmt, VarDecl):
352
+ # Kept for the ta.* length lowering: an explicit
353
+ # ``series`` makes TradingView re-window the call.
354
+ stmt.annotations = {**(stmt.annotations or {}),
355
+ "qualifier": qualifier}
356
+ if self._library and qualifier == "const" and isinstance(stmt, VarDecl):
357
+ # A library's ``const`` declaration: the only globals an
358
+ # exported function may read, and a v5 const operand.
359
+ stmt.annotations = {**(stmt.annotations or {}),
360
+ "declared_const": True}
361
+ return stmt
248
362
 
249
363
  # Type-annotated declaration: float x = ..., int x = ...
250
364
  if cur.type in TYPE_KEYWORDS and self._peek().type == TokenType.IDENT:
@@ -292,6 +406,9 @@ class Parser:
292
406
  # strategy() / indicator() declaration
293
407
  if cur.value in ("strategy", "indicator") and self._peek().type == TokenType.LPAREN:
294
408
  return self._parse_strategy_decl()
409
+ if (self._library and cur.value == "library"
410
+ and self._peek().type == TokenType.LPAREN):
411
+ return self._parse_strategy_decl()
295
412
 
296
413
  # Check for function definition: name(params) =>
297
414
  if self._is_func_def():
@@ -350,8 +467,13 @@ class Parser:
350
467
  return False
351
468
  if cur.value in ("enum", "type", "strategy", "indicator", "na", "true", "false"):
352
469
  return False
353
- # Skip past optional generic args after the type ident: IDENT [< ... >]
354
470
  i = base + 1
471
+ # A library type is qualified by its import alias: ``lib.Type x = ...``.
472
+ while (i + 1 < len(self.tokens)
473
+ and self.tokens[i].type == TokenType.DOT
474
+ and self.tokens[i + 1].type == TokenType.IDENT):
475
+ i += 2
476
+ # Skip past optional generic args after the type ident: IDENT [< ... >]
355
477
  if i < len(self.tokens) and self.tokens[i].type == TokenType.LT:
356
478
  depth = 1
357
479
  i += 1
@@ -364,6 +486,12 @@ class Parser:
364
486
  elif tt in (TokenType.NEWLINE, TokenType.EOF_TOKEN):
365
487
  return False
366
488
  i += 1
489
+ # Postfix-array shorthand of a non-keyword type: ``color[] c = ...``,
490
+ # ``line[] ls = ...``. An empty ``[]`` cannot be a history subscript.
491
+ while (i + 1 < len(self.tokens)
492
+ and self.tokens[i].type == TokenType.LBRACKET
493
+ and self.tokens[i + 1].type == TokenType.RBRACKET):
494
+ i += 2
367
495
  # Now expect an IDENT (variable name).
368
496
  if i >= len(self.tokens) or self.tokens[i].type != TokenType.IDENT:
369
497
  return False
@@ -414,18 +542,58 @@ class Parser:
414
542
  }
415
543
  return self._set_loc(node, start_tok)
416
544
 
545
+ def _parse_export_decl(self):
546
+ """Parse a library's ``export`` declaration: a function, a method, a
547
+ type, an enum or a ``const`` variable, annotated ``exported``."""
548
+ export_tok = self._advance() # consume 'export'
549
+ cur = self._current()
550
+ node = None
551
+ if cur.type == TokenType.METHOD:
552
+ node = self._parse_method_def()
553
+ elif cur.type == TokenType.IDENT:
554
+ if (cur.value in ("type", "enum")
555
+ and self._peek().type == TokenType.IDENT
556
+ and self._peek(2).type == TokenType.NEWLINE):
557
+ node = self._parse_type_or_enum_decl()
558
+ elif cur.value == "const":
559
+ node = self._parse_single_statement()
560
+ if not isinstance(node, VarDecl):
561
+ node = None
562
+ elif self._is_func_def():
563
+ node = self._parse_func_def()
564
+ if node is None:
565
+ raise ParseError(
566
+ "'export' must precede a function, method, type, enum or "
567
+ "const declaration", export_tok,
568
+ )
569
+ node.annotations = {**(node.annotations or {}), "exported": True}
570
+ return node
571
+
417
572
  def _parse_import_stmt(self) -> ImportStmt:
418
- """Parse: import path/to/library/version"""
573
+ """Parse: import <user>/<name>/<version> [as <alias>]"""
419
574
  start_tok = self._current()
420
575
  self._consume(TokenType.IMPORT)
421
- # Consume the rest of the line as the import path
422
- parts: list[str] = []
576
+ # Consume the rest of the line; any other spelling keeps it as
577
+ # written in ``path`` for the refusal.
578
+ tokens = []
423
579
  while (not self._at_end()
424
580
  and not self._check(TokenType.NEWLINE)
425
581
  and not self._check(TokenType.EOF_TOKEN)):
426
- parts.append(self._advance().value)
427
- path = "".join(parts)
428
- node = ImportStmt(path=path)
582
+ tokens.append(self._advance())
583
+ values = [tok.value for tok in tokens]
584
+ node = ImportStmt(path="".join(values))
585
+
586
+ def word(index: int) -> bool:
587
+ return bool(re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", values[index]))
588
+
589
+ if (len(tokens) in (5, 7) and word(0) and word(2)
590
+ and values[1] == values[3] == "/"
591
+ and tokens[4].type == TokenType.NUMBER and values[4].isdigit()
592
+ and (len(tokens) == 5 or (values[5] == "as" and word(6)))):
593
+ node.user, node.name, node.version = values[0], values[2], int(values[4])
594
+ node.path = f"{node.user}/{node.name}/{node.version}"
595
+ if len(tokens) == 7:
596
+ node.alias = values[6]
429
597
  return self._set_loc(node, start_tok)
430
598
 
431
599
  def _parse_var_decl(self) -> VarDecl | list:
@@ -477,6 +645,10 @@ class Parser:
477
645
  def _parse_type_hint_string(self) -> str:
478
646
  """Parse primitive, UDT, array<T>, map<K,V>, or postfix-array (``T[]``) hints."""
479
647
  base = self._advance().value
648
+ # Library type qualified by its import alias: ``lib.Type``.
649
+ while self._check(TokenType.DOT) and self._peek().type == TokenType.IDENT:
650
+ self._advance() # .
651
+ base = f"{base}.{self._advance().value}"
480
652
  if self._check(TokenType.LT):
481
653
  parts: list[str] = []
482
654
  depth = 0
@@ -549,6 +721,10 @@ class Parser:
549
721
  depth = 0
550
722
  i = self.pos
551
723
  while i < len(self.tokens):
724
+ # One look-ahead per ``.name<`` scans to the end of the line, so a
725
+ # long line of comparisons costs its length squared.
726
+ if self._budget is not None and (i - self.pos) % 1024 == 1023:
727
+ self._budget.check(self._loc(self.tokens[self.pos]), Phase.PARSER)
552
728
  tt = self.tokens[i].type
553
729
  if tt == TokenType.LT:
554
730
  depth += 1
@@ -586,6 +762,12 @@ class Parser:
586
762
  # this branch the ``[]`` is left unconsumed, the name fails to
587
763
  # parse, and the whole declaration is silently dropped.
588
764
  type_hint = self._parse_type_hint_string()
765
+ elif (self._current().type == TokenType.IDENT
766
+ and self._peek().type == TokenType.DOT
767
+ and self._is_ident_typed_var_decl()):
768
+ # Library type qualified by its import alias:
769
+ # ``var lib.Type name = ...``.
770
+ type_hint = self._parse_type_hint_string()
589
771
 
590
772
  name_tok = self._consume(TokenType.IDENT)
591
773
  self._consume(TokenType.EQUALS)
@@ -664,10 +846,13 @@ class Parser:
664
846
  """
665
847
  TYPE_TOKENS = {TokenType.TYPE_INT, TokenType.TYPE_FLOAT,
666
848
  TokenType.TYPE_BOOL, TokenType.TYPE_STRING}
667
- # Optional qualifiers — they do not affect the C++ param type.
668
- while self._check(TokenType.IDENT) and self._current().value in (
669
- "series", "simple", "const",
670
- ):
849
+ # Optional qualifiers — they do not affect the C++ param type. A
850
+ # qualifier word followed by ``,`` ``)`` or ``=`` is the parameter's
851
+ # own name.
852
+ while (self._check(TokenType.IDENT)
853
+ and self._current().value in ("series", "simple", "const")
854
+ and self._peek().type not in (
855
+ TokenType.COMMA, TokenType.RPAREN, TokenType.EQUALS)):
671
856
  self._advance()
672
857
  # Is there a type annotation before the parameter name? A builtin type
673
858
  # token always is; an IDENT is a type only if followed by another IDENT
@@ -683,6 +868,19 @@ class Parser:
683
868
  nxt = self._peek().type
684
869
  if nxt in (TokenType.IDENT, TokenType.LBRACKET, TokenType.LT):
685
870
  has_type = True
871
+ elif nxt == TokenType.DOT:
872
+ # A library type qualified by its import alias:
873
+ # ``lib.Type name`` / ``lib.Type[] names``.
874
+ i = self.pos + 1
875
+ while (i + 1 < len(self.tokens)
876
+ and self.tokens[i].type == TokenType.DOT
877
+ and self.tokens[i + 1].type == TokenType.IDENT):
878
+ i += 2
879
+ has_type = (
880
+ i < len(self.tokens)
881
+ and self.tokens[i].type in (
882
+ TokenType.IDENT, TokenType.LBRACKET, TokenType.LT)
883
+ )
686
884
  if not has_type:
687
885
  return None
688
886
  return self._parse_type_hint_string()
@@ -695,7 +893,9 @@ class Parser:
695
893
  params = []
696
894
  param_type_hints: list = []
697
895
  param_defaults: list = []
896
+ param_qualifiers: list = []
698
897
  while not self._check(TokenType.RPAREN):
898
+ param_qualifiers.append(self._param_qualifier_ahead())
699
899
  # Consume the optional type annotation (builtin / user / drawing /
700
900
  # ``T[]``), returning the canonical hint string. Handles ``float[] arr``,
701
901
  # ``line[] ln``, ``color c``, ``SDZone z``, ``string tf``, as well as
@@ -732,8 +932,24 @@ class Parser:
732
932
  "param_type_hints": param_type_hints,
733
933
  "param_defaults": param_defaults,
734
934
  }
935
+ if self._library:
936
+ # A library may overload a function by its parameters' qualifiers
937
+ # alone (TradingView/Request/3: ``simple string`` and ``series
938
+ # string``); ``library_inline`` picks the overload by them.
939
+ node.annotations["param_qualifiers"] = param_qualifiers
735
940
  return self._set_loc(node, start_tok)
736
941
 
942
+ def _param_qualifier_ahead(self) -> str | None:
943
+ """The ``series`` / ``simple`` / ``const`` word a parameter's
944
+ annotation starts with (``_parse_param_type_annotation`` consumes
945
+ it), or None."""
946
+ cur = self._current()
947
+ if (cur.type == TokenType.IDENT and cur.value in ("series", "simple", "const")
948
+ and self._peek().type not in (
949
+ TokenType.COMMA, TokenType.RPAREN, TokenType.EQUALS)):
950
+ return cur.value
951
+ return None
952
+
737
953
  def _parse_type_or_enum_decl(self):
738
954
  """Parse type or enum block declarations."""
739
955
  start_tok = self._current()
@@ -811,14 +1027,10 @@ class Parser:
811
1027
  # args. See data/validation/udt-method-probe-04-default-param.
812
1028
  param_defaults: list = [None]
813
1029
  while self._match(TokenType.COMMA):
814
- # Skip optional type annotations
815
- param_type = None
816
- if self._current().type in TYPE_KEYWORDS:
817
- param_type = self._parse_type_hint_string()
818
- elif (self._current().type == TokenType.IDENT
819
- and self._peek().type in (TokenType.IDENT, TokenType.LBRACKET, TokenType.LT)):
820
- # ``line ln`` / ``float[] arr`` / ``array<float> xs`` typed param.
821
- param_type = self._parse_type_hint_string()
1030
+ # Optional ``series``/``simple``/``const`` qualifiers and type
1031
+ # annotation: ``line ln`` / ``float[] arr`` / ``array<float> xs``
1032
+ # / ``series float x`` / ``lib.Type t``.
1033
+ param_type = self._parse_param_type_annotation()
822
1034
  p = self._consume(TokenType.IDENT).value
823
1035
  pdefault = None
824
1036
  if self._check(TokenType.EQUALS):
@@ -863,8 +1075,13 @@ class Parser:
863
1075
  if self._check(TokenType.ELSE):
864
1076
  self._advance()
865
1077
  if self._check(TokenType.IF):
866
- # else if -> nested IfStmt in else_body
867
- else_body = [self._parse_if_stmt()]
1078
+ # else if -> nested IfStmt in else_body. The ladder recurses
1079
+ # once per branch at one indentation.
1080
+ self._enter(self._current())
1081
+ try:
1082
+ else_body = [self._parse_if_stmt()]
1083
+ finally:
1084
+ self._depth -= 1
868
1085
  else:
869
1086
  self._consume(TokenType.NEWLINE)
870
1087
  self._consume(TokenType.INDENT)
@@ -959,7 +1176,7 @@ class Parser:
959
1176
  default_body = self._parse_block()
960
1177
  self._consume(TokenType.DEDENT)
961
1178
  else:
962
- default_body = [ExprStmt(expr=self._parse_expression())]
1179
+ default_body = self._parse_arm_line()
963
1180
  else:
964
1181
  # case_expr => body
965
1182
  case_expr = self._parse_expression()
@@ -970,7 +1187,7 @@ class Parser:
970
1187
  case_body = self._parse_block()
971
1188
  self._consume(TokenType.DEDENT)
972
1189
  else:
973
- case_body = [ExprStmt(expr=self._parse_expression())]
1190
+ case_body = self._parse_arm_line()
974
1191
  cases.append((case_expr, case_body))
975
1192
  self._skip_newlines()
976
1193
 
@@ -978,9 +1195,74 @@ class Parser:
978
1195
  node = SwitchStmt(expr=expr, cases=cases, default_body=default_body)
979
1196
  return self._set_loc(node, start_tok)
980
1197
 
1198
+ def _parse_arm_line(self) -> list:
1199
+ """The block of a ``switch`` arm written on its ``=>`` line.
1200
+
1201
+ Pine joins one-line statements with commas, and an arm's line is such
1202
+ a block: TradingView's ``TradingView/Request/3`` library ends an arm
1203
+ in ``=> runtime.error(...), ""``. TradingView runs the statements left
1204
+ to right and the arm's value is the last one's, as for a block written
1205
+ below the arrow; a declaration or an assignment may be one of them
1206
+ (``tests/fixtures/tail_f_tv``). A lone expression is the node an arm
1207
+ always held.
1208
+ """
1209
+ body: list = []
1210
+ while True:
1211
+ if self._arm_line_statement_ahead():
1212
+ self._extend_statement_list(body, self._parse_single_statement())
1213
+ else:
1214
+ start_tok = self._current()
1215
+ expr = self._parse_expression()
1216
+ if self._current().type in COMPOUND_ASSIGN_OPS:
1217
+ # A field or element target: ``o.f := v``.
1218
+ op = COMPOUND_ASSIGN_OPS[self._advance().type]
1219
+ node = Assignment(target=expr, op=op, value=self._parse_expression())
1220
+ body.append(self._set_loc(node, start_tok))
1221
+ else:
1222
+ body.append(ExprStmt(expr=expr))
1223
+ if not self._match(TokenType.COMMA):
1224
+ return body
1225
+ if self._check(TokenType.NEWLINE) or self._check(TokenType.DEDENT) or self._at_end():
1226
+ return body
1227
+
1228
+ def _arm_line_statement_ahead(self) -> bool:
1229
+ """Whether an arm line's next statement is one no expression starts:
1230
+ a declaration, an assignment to a name, ``break`` or ``continue``.
1231
+ (A call is never a function definition here, so ``f(a)`` before the
1232
+ next arm's ``=>`` stays a call.)"""
1233
+ cur, nxt = self._current(), self._peek()
1234
+ if cur.type in (TokenType.VAR, TokenType.VARIP,
1235
+ TokenType.BREAK, TokenType.CONTINUE):
1236
+ return True
1237
+ if cur.type in TYPE_KEYWORDS:
1238
+ return ((nxt.type == TokenType.IDENT
1239
+ and self._peek(2).type == TokenType.EQUALS)
1240
+ or (nxt.type == TokenType.LBRACKET
1241
+ and self._peek(2).type == TokenType.RBRACKET))
1242
+ if cur.type == TokenType.LBRACKET:
1243
+ return self._is_tuple_assign()
1244
+ if cur.type != TokenType.IDENT:
1245
+ return False
1246
+ if cur.value in ("const", "series", "simple") and (
1247
+ nxt.type in TYPE_KEYWORDS
1248
+ or (nxt.type == TokenType.IDENT
1249
+ and self._is_ident_typed_var_decl(offset=1))):
1250
+ return True
1251
+ return (self._is_ident_typed_var_decl()
1252
+ or (nxt.type == TokenType.EQUALS
1253
+ and self._peek(2).type != TokenType.EQUALS)
1254
+ or nxt.type in COMPOUND_ASSIGN_OPS)
1255
+
981
1256
  # -- Block parsing --
982
1257
 
983
1258
  def _parse_block(self) -> list:
1259
+ self._enter(self._current())
1260
+ try:
1261
+ return self._parse_block_statements()
1262
+ finally:
1263
+ self._depth -= 1
1264
+
1265
+ def _parse_block_statements(self) -> list:
984
1266
  stmts: list = []
985
1267
  self._skip_newlines()
986
1268
  while not self._check(TokenType.DEDENT) and not self._at_end():
@@ -991,8 +1273,9 @@ class Parser:
991
1273
  stmts.extend(stmt)
992
1274
  else:
993
1275
  stmts.append(stmt)
994
- except ParseError:
995
- self._recover()
1276
+ self._expect_statement_end()
1277
+ except ParseError as error:
1278
+ self._raise_syntax_error(error)
996
1279
  self._skip_newlines()
997
1280
  return stmts
998
1281
 
@@ -1020,12 +1303,16 @@ class Parser:
1020
1303
  }
1021
1304
 
1022
1305
  def _parse_expression(self):
1023
- # if/switch can be used as expressions (RHS of assignments)
1024
- if self._check(TokenType.IF):
1025
- return self._parse_if_expr()
1026
- if self._check(TokenType.SWITCH):
1027
- return self._parse_switch_expr()
1028
- return self._parse_ternary()
1306
+ self._enter(self._current())
1307
+ try:
1308
+ # if/switch can be used as expressions (RHS of assignments)
1309
+ if self._check(TokenType.IF):
1310
+ return self._parse_if_expr()
1311
+ if self._check(TokenType.SWITCH):
1312
+ return self._parse_switch_expr()
1313
+ return self._parse_ternary()
1314
+ finally:
1315
+ self._depth -= 1
1029
1316
 
1030
1317
  def _parse_if_expr(self):
1031
1318
  """Parse if/else as an expression (returns IfStmt, codegen handles it)."""
@@ -1073,12 +1360,14 @@ class Parser:
1073
1360
  right = self._parse_and()
1074
1361
  left = BinOp(left=left, op="or", right=right)
1075
1362
  self._set_loc(left, start_tok)
1363
+ self._check_chain(left, start_tok)
1076
1364
  elif not in_continuation and self._try_line_continuation(TokenType.OR):
1077
1365
  in_continuation = True
1078
1366
  self._advance() # consume OR
1079
1367
  right = self._parse_and()
1080
1368
  left = BinOp(left=left, op="or", right=right)
1081
1369
  self._set_loc(left, start_tok)
1370
+ self._check_chain(left, start_tok)
1082
1371
  else:
1083
1372
  break
1084
1373
  if in_continuation:
@@ -1095,12 +1384,14 @@ class Parser:
1095
1384
  right = self._parse_not()
1096
1385
  left = BinOp(left=left, op="and", right=right)
1097
1386
  self._set_loc(left, start_tok)
1387
+ self._check_chain(left, start_tok)
1098
1388
  elif not in_continuation and self._try_line_continuation(TokenType.AND):
1099
1389
  in_continuation = True
1100
1390
  self._advance() # consume AND
1101
1391
  right = self._parse_not()
1102
1392
  left = BinOp(left=left, op="and", right=right)
1103
1393
  self._set_loc(left, start_tok)
1394
+ self._check_chain(left, start_tok)
1104
1395
  else:
1105
1396
  break
1106
1397
  if in_continuation:
@@ -1116,8 +1407,12 @@ class Parser:
1116
1407
  def _parse_not(self):
1117
1408
  if self._check(TokenType.NOT):
1118
1409
  start_tok = self._current()
1410
+ self._enter(start_tok)
1119
1411
  self._advance()
1120
- operand = self._parse_not()
1412
+ try:
1413
+ operand = self._parse_not()
1414
+ finally:
1415
+ self._depth -= 1
1121
1416
  node = UnaryOp(op="not", operand=operand)
1122
1417
  return self._set_loc(node, start_tok)
1123
1418
  return self._parse_comparison()
@@ -1135,6 +1430,7 @@ class Parser:
1135
1430
  right = self._parse_addition()
1136
1431
  left = BinOp(left=left, op=op, right=right)
1137
1432
  self._set_loc(left, start_tok)
1433
+ self._check_chain(left, start_tok)
1138
1434
  return left
1139
1435
 
1140
1436
  def _parse_addition(self):
@@ -1145,6 +1441,7 @@ class Parser:
1145
1441
  right = self._parse_multiplication()
1146
1442
  left = BinOp(left=left, op=op, right=right)
1147
1443
  self._set_loc(left, start_tok)
1444
+ self._check_chain(left, start_tok)
1148
1445
  return left
1149
1446
 
1150
1447
  def _parse_multiplication(self):
@@ -1156,24 +1453,34 @@ class Parser:
1156
1453
  right = self._parse_unary()
1157
1454
  left = BinOp(left=left, op=op, right=right)
1158
1455
  self._set_loc(left, start_tok)
1456
+ self._check_chain(left, start_tok)
1159
1457
  return left
1160
1458
 
1161
1459
  def _parse_unary(self):
1162
1460
  if self._check(TokenType.MINUS):
1163
1461
  start_tok = self._current()
1462
+ self._enter(start_tok)
1164
1463
  self._advance()
1165
- operand = self._parse_unary()
1464
+ try:
1465
+ operand = self._parse_unary()
1466
+ finally:
1467
+ self._depth -= 1
1166
1468
  node = UnaryOp(op="-", operand=operand)
1167
1469
  return self._set_loc(node, start_tok)
1168
1470
  if self._check(TokenType.PLUS):
1169
1471
  start_tok = self._current()
1472
+ self._enter(start_tok)
1170
1473
  self._advance()
1171
- operand = self._parse_unary()
1474
+ try:
1475
+ operand = self._parse_unary()
1476
+ finally:
1477
+ self._depth -= 1
1172
1478
  node = UnaryOp(op="+", operand=operand)
1173
1479
  return self._set_loc(node, start_tok)
1174
1480
  return self._parse_postfix()
1175
1481
 
1176
1482
  def _parse_postfix(self):
1483
+ chain_tok = self._current()
1177
1484
  expr = self._parse_primary()
1178
1485
  while True:
1179
1486
  # Subscript: expr[index]
@@ -1212,6 +1519,7 @@ class Parser:
1212
1519
  expr = self._parse_call_with_callee(expr)
1213
1520
  else:
1214
1521
  break
1522
+ self._check_chain(expr, chain_tok)
1215
1523
  return expr
1216
1524
 
1217
1525
  def _is_call_position(self, expr) -> bool:
@@ -1234,7 +1542,7 @@ class Parser:
1234
1542
  node.annotations = {"call_arg_order": call_arg_order}
1235
1543
  return self._set_loc(node, start_tok)
1236
1544
 
1237
- def _parse_call_args(self) -> tuple[list, dict, list]:
1545
+ def _parse_call_args(self) -> tuple[list, dict, ArgOrder]:
1238
1546
  """Parse function call arguments and keyword arguments."""
1239
1547
  args: list = []
1240
1548
  kwargs: dict = {}
@@ -1275,7 +1583,7 @@ class Parser:
1275
1583
 
1276
1584
  self._match(TokenType.COMMA)
1277
1585
 
1278
- return args, kwargs, call_arg_order
1586
+ return args, kwargs, ArgOrder(call_arg_order)
1279
1587
 
1280
1588
  # -- Primary expressions --
1281
1589
 
@@ -1304,10 +1612,20 @@ class Parser:
1304
1612
  # Number literal
1305
1613
  if cur.type == TokenType.NUMBER:
1306
1614
  self._advance()
1307
- if "." in cur.value or "e" in cur.value or "E" in cur.value:
1308
- val = float(cur.value)
1309
- else:
1310
- val = int(cur.value)
1615
+ try:
1616
+ if "." in cur.value or "e" in cur.value or "E" in cur.value:
1617
+ val = float(cur.value)
1618
+ out_of_range = not math.isfinite(val)
1619
+ else:
1620
+ val = int(cur.value)
1621
+ out_of_range = val > (1 << 64) - 1
1622
+ except (ValueError, OverflowError):
1623
+ out_of_range = True
1624
+ if out_of_range:
1625
+ raise limit_error(
1626
+ "Numeric literal exceeds the generated C++ range.",
1627
+ self._loc(cur), Phase.PARSER,
1628
+ )
1311
1629
  node = NumberLiteral(value=val)
1312
1630
  return self._set_loc(node, cur)
1313
1631
 
@@ -1356,6 +1674,4 @@ class Parser:
1356
1674
  node = Identifier(name=cur.value)
1357
1675
  return self._set_loc(node, cur)
1358
1676
 
1359
- raise ParseError(
1360
- f"Unexpected token {cur.type.name}({cur.value!r}) at L{cur.line}:{cur.col}"
1361
- )
1677
+ raise ParseError(f"Unexpected token {cur.type.name}({cur.value!r})", cur)