@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.
- package/README.md +16 -16
- package/glue.py +24 -16
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +125 -34
- package/pineforge_codegen/analyzer/__init__.py +2 -0
- package/pineforge_codegen/analyzer/base.py +754 -76
- package/pineforge_codegen/analyzer/call_handlers.py +260 -40
- package/pineforge_codegen/analyzer/contracts.py +37 -0
- package/pineforge_codegen/analyzer/diagnostics.py +30 -4
- package/pineforge_codegen/analyzer/tables.py +49 -8
- package/pineforge_codegen/analyzer/types.py +33 -1
- package/pineforge_codegen/ast_nodes.py +32 -1
- package/pineforge_codegen/block_locals.py +185 -0
- package/pineforge_codegen/builtin_keywords.py +42 -0
- package/pineforge_codegen/codegen/base.py +896 -156
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +946 -213
- package/pineforge_codegen/codegen/helpers.py +435 -14
- package/pineforge_codegen/codegen/host_members.py +162 -0
- package/pineforge_codegen/codegen/input.py +252 -85
- package/pineforge_codegen/codegen/security.py +4372 -377
- package/pineforge_codegen/codegen/session_market.py +71 -0
- package/pineforge_codegen/codegen/ta.py +1188 -100
- package/pineforge_codegen/codegen/tables.py +193 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +1882 -78
- package/pineforge_codegen/codegen/visit_call.py +920 -131
- package/pineforge_codegen/codegen/visit_expr.py +738 -57
- package/pineforge_codegen/codegen/visit_stmt.py +595 -49
- package/pineforge_codegen/external_requests.py +877 -0
- package/pineforge_codegen/lexer.py +104 -22
- package/pineforge_codegen/library_inline.py +1304 -0
- package/pineforge_codegen/library_modules.py +126 -0
- package/pineforge_codegen/library_v5.py +683 -0
- package/pineforge_codegen/limits.py +138 -0
- package/pineforge_codegen/method_binding.py +33 -0
- package/pineforge_codegen/parser.py +384 -68
- package/pineforge_codegen/pine_libraries.py +266 -0
- package/pineforge_codegen/pine_spelling.py +216 -0
- package/pineforge_codegen/pragmas.py +64 -10
- package/pineforge_codegen/security_contexts.py +1585 -0
- package/pineforge_codegen/session_reads.py +84 -0
- package/pineforge_codegen/signatures.py +48 -23
- package/pineforge_codegen/support_checker.py +1106 -85
- package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +23 -21
- package/transpile.worker.mjs +24 -16
- 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,
|
|
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
|
-
|
|
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>"
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
self.
|
|
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
|
-
"""
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
422
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
node = ImportStmt(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
|
-
|
|
669
|
-
|
|
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
|
-
#
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
995
|
-
|
|
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
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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)
|