pine2pyne 0.2.4__py3-none-any.whl
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.
- pine2pyne/__init__.py +84 -0
- pine2pyne/__main__.py +12 -0
- pine2pyne/ast_nodes.py +405 -0
- pine2pyne/ast_walk.py +93 -0
- pine2pyne/block_value_lowering.py +694 -0
- pine2pyne/bool_builtins.py +42 -0
- pine2pyne/bool_value_typing.py +580 -0
- pine2pyne/cli.py +247 -0
- pine2pyne/codegen.py +886 -0
- pine2pyne/compat/__init__.py +19 -0
- pine2pyne/compat/__main__.py +6 -0
- pine2pyne/compat/cli.py +114 -0
- pine2pyne/compat/comparison.py +105 -0
- pine2pyne/compat/corpus.py +73 -0
- pine2pyne/compat/report.py +95 -0
- pine2pyne/compat/runner.py +63 -0
- pine2pyne/condition_typing.py +260 -0
- pine2pyne/conditional_history_calls.py +96 -0
- pine2pyne/core/__init__.py +26 -0
- pine2pyne/core/contracts.py +125 -0
- pine2pyne/di.py +173 -0
- pine2pyne/emit/__init__.py +5 -0
- pine2pyne/emit/legacy.py +14 -0
- pine2pyne/emit/pynecomp/__init__.py +20 -0
- pine2pyne/emit/pynecomp/banner.py +30 -0
- pine2pyne/emit/pynecomp/blank_lines.py +69 -0
- pine2pyne/emit/pynecomp/emitter.py +61 -0
- pine2pyne/emit/pynecomp/imports.py +65 -0
- pine2pyne/emit/pynecomp/passes/__init__.py +84 -0
- pine2pyne/emit/pynecomp/passes/annotations.py +422 -0
- pine2pyne/emit/pynecomp/passes/base.py +69 -0
- pine2pyne/emit/pynecomp/passes/builtins.py +169 -0
- pine2pyne/emit/pynecomp/passes/hoisting.py +205 -0
- pine2pyne/emit/pynecomp/passes/imports.py +112 -0
- pine2pyne/emit/pynecomp/passes/legacy_colors.py +101 -0
- pine2pyne/emit/pynecomp/passes/library.py +205 -0
- pine2pyne/emit/pynecomp/passes/rewrites.py +215 -0
- pine2pyne/emit/pynecomp/printer.py +288 -0
- pine2pyne/emit/pynecomp/unparser.py +186 -0
- pine2pyne/emitted_names.py +152 -0
- pine2pyne/errors.py +69 -0
- pine2pyne/for_loop_end_bound.py +172 -0
- pine2pyne/grouped_history_lowering.py +310 -0
- pine2pyne/import_resolver.py +469 -0
- pine2pyne/input_hoisting.py +195 -0
- pine2pyne/legacy_function_names.py +74 -0
- pine2pyne/legacy_sessions.py +68 -0
- pine2pyne/lexer.py +630 -0
- pine2pyne/library_collision.py +434 -0
- pine2pyne/namespace_members.py +247 -0
- pine2pyne/node_ids.py +235 -0
- pine2pyne/once_lowering.py +75 -0
- pine2pyne/overload_resolution.py +475 -0
- pine2pyne/parse_repair.py +398 -0
- pine2pyne/parser.py +1873 -0
- pine2pyne/pine_builtins.py +394 -0
- pine2pyne/pine_semantics.py +604 -0
- pine2pyne/pipeline/__init__.py +18 -0
- pine2pyne/pipeline/service.py +70 -0
- pine2pyne/pipeline/stages.py +73 -0
- pine2pyne/scope_renamer.py +965 -0
- pine2pyne/strategy_when_lowering.py +101 -0
- pine2pyne/strict_boolean.py +101 -0
- pine2pyne/symbol_table.py +166 -0
- pine2pyne/tokens.py +143 -0
- pine2pyne/transformer.py +2510 -0
- pine2pyne/type_inference.py +422 -0
- pine2pyne/udt_field_defaults.py +149 -0
- pine2pyne/value_blocks.py +357 -0
- pine2pyne-0.2.4.dist-info/METADATA +99 -0
- pine2pyne-0.2.4.dist-info/RECORD +75 -0
- pine2pyne-0.2.4.dist-info/WHEEL +5 -0
- pine2pyne-0.2.4.dist-info/entry_points.txt +3 -0
- pine2pyne-0.2.4.dist-info/licenses/LICENSE +201 -0
- pine2pyne-0.2.4.dist-info/top_level.txt +1 -0
pine2pyne/parser.py
ADDED
|
@@ -0,0 +1,1873 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recursive descent parser for Pine Script v6.
|
|
3
|
+
|
|
4
|
+
Converts token stream from lexer into an Abstract Syntax Tree (AST).
|
|
5
|
+
"""
|
|
6
|
+
import dataclasses
|
|
7
|
+
import warnings
|
|
8
|
+
from typing import List, Optional, Union
|
|
9
|
+
from .tokens import Token, TokenType
|
|
10
|
+
from .ast_nodes import *
|
|
11
|
+
from .errors import ParserError, SemanticError, UnsupportedFeatureError
|
|
12
|
+
from .once_lowering import lower_once_structures
|
|
13
|
+
from .strategy_when_lowering import lower_strategy_when_arguments
|
|
14
|
+
from .scope_renamer import fresh_name
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _pine_version(version: Optional[VersionAnnotation]) -> int:
|
|
18
|
+
"""The //@version number, or 0 when absent or not a number."""
|
|
19
|
+
text = version.version if version else ''
|
|
20
|
+
return int(text) if str(text).isdigit() else 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# `x += 5` is read as `x := x + 5`: the operator each compound assignment token stands for
|
|
24
|
+
COMPOUND_ASSIGNMENT_OPERATORS = {
|
|
25
|
+
TokenType.PLUS_ASSIGN: '+',
|
|
26
|
+
TokenType.MINUS_ASSIGN: '-',
|
|
27
|
+
TokenType.MULT_ASSIGN: '*',
|
|
28
|
+
TokenType.DIV_ASSIGN: '/',
|
|
29
|
+
TokenType.MOD_ASSIGN: '%',
|
|
30
|
+
}
|
|
31
|
+
# The variable that holds the receiver of `arr.get(0).v += 1` (card #98)
|
|
32
|
+
RECEIVER_NAME_STEM = 'assigned_field_receiver'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _as_statement_list(statement) -> list:
|
|
36
|
+
"""What parse_statement returned as a list: one statement, several (a source statement
|
|
37
|
+
lowered to more than one, card #98), or none."""
|
|
38
|
+
if isinstance(statement, list):
|
|
39
|
+
return statement
|
|
40
|
+
return [statement] if statement else []
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Parser:
|
|
44
|
+
"""Recursive descent parser for Pine Script v6."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, tokens: List[Token]):
|
|
47
|
+
# COMPILER_ANNOTATION tokens (//@variable, //@param, //@function, ...) are
|
|
48
|
+
# documentation only and carry no syntax. They can appear anywhere a
|
|
49
|
+
# comment can — including inside blocks and expressions — so handling them
|
|
50
|
+
# at each parse site would mean a case in every rule. Drop them up front
|
|
51
|
+
# instead; the meaningful VERSION_ANNOTATION is preserved.
|
|
52
|
+
self.tokens = [t for t in tokens if t.type is not TokenType.COMPILER_ANNOTATION]
|
|
53
|
+
self.pos = 0
|
|
54
|
+
self._token_names: Optional[set] = None # every name the tokens hold, read on first use (card #98)
|
|
55
|
+
|
|
56
|
+
def current_token(self) -> Token:
|
|
57
|
+
"""Get current token without advancing."""
|
|
58
|
+
if self.pos >= len(self.tokens):
|
|
59
|
+
return self.tokens[-1] # Return EOF
|
|
60
|
+
return self.tokens[self.pos]
|
|
61
|
+
|
|
62
|
+
def peek_token(self, offset: int = 1) -> Token:
|
|
63
|
+
"""Look ahead at token at pos + offset."""
|
|
64
|
+
pos = self.pos + offset
|
|
65
|
+
if pos >= len(self.tokens):
|
|
66
|
+
return self.tokens[-1] # Return EOF
|
|
67
|
+
return self.tokens[pos]
|
|
68
|
+
|
|
69
|
+
def advance(self) -> Token:
|
|
70
|
+
"""Consume and return current token."""
|
|
71
|
+
token = self.current_token()
|
|
72
|
+
if token.type != TokenType.EOF:
|
|
73
|
+
self.pos += 1
|
|
74
|
+
return token
|
|
75
|
+
|
|
76
|
+
def expect(self, token_type: TokenType) -> Token:
|
|
77
|
+
"""Consume token of expected type or raise error."""
|
|
78
|
+
token = self.current_token()
|
|
79
|
+
if token.type != token_type:
|
|
80
|
+
error = ParserError(
|
|
81
|
+
f"Expected {token_type.name}, got {token.type.name}",
|
|
82
|
+
token.line,
|
|
83
|
+
token.column
|
|
84
|
+
)
|
|
85
|
+
error.token_index = self._current_index() # read by parse_repair (card #55)
|
|
86
|
+
raise error
|
|
87
|
+
return self.advance()
|
|
88
|
+
|
|
89
|
+
def match(self, *token_types: TokenType) -> bool:
|
|
90
|
+
"""Check if current token matches any of the given types."""
|
|
91
|
+
return self.current_token().type in token_types
|
|
92
|
+
|
|
93
|
+
def skip_newlines(self) -> None:
|
|
94
|
+
"""Skip any NEWLINE tokens."""
|
|
95
|
+
while self.match(TokenType.NEWLINE):
|
|
96
|
+
self.advance()
|
|
97
|
+
|
|
98
|
+
def _looks_like_generic_call(self) -> bool:
|
|
99
|
+
"""
|
|
100
|
+
Check if the current position looks like a generic function call.
|
|
101
|
+
Pattern: <type1, type2, ...>(args)
|
|
102
|
+
This prevents `close < ema` from being parsed as generics.
|
|
103
|
+
"""
|
|
104
|
+
saved_pos = self.pos
|
|
105
|
+
try:
|
|
106
|
+
# We're currently at <, skip it
|
|
107
|
+
self.pos += 1
|
|
108
|
+
|
|
109
|
+
# Skip through the generic parameters
|
|
110
|
+
depth = 1
|
|
111
|
+
while depth > 0 and self.pos < len(self.tokens):
|
|
112
|
+
token = self.tokens[self.pos]
|
|
113
|
+
if token.type == TokenType.LT:
|
|
114
|
+
depth += 1
|
|
115
|
+
elif token.type == TokenType.GT:
|
|
116
|
+
depth -= 1
|
|
117
|
+
if depth == 0:
|
|
118
|
+
# Found closing >, check next token
|
|
119
|
+
self.pos += 1
|
|
120
|
+
if self.pos < len(self.tokens):
|
|
121
|
+
next_token = self.tokens[self.pos]
|
|
122
|
+
# Generic calls MUST be followed by ( for function call
|
|
123
|
+
self.pos = saved_pos
|
|
124
|
+
return next_token.type == TokenType.LPAREN
|
|
125
|
+
self.pos = saved_pos
|
|
126
|
+
return False
|
|
127
|
+
elif token.type not in (TokenType.TYPE_IDENTIFIER, TokenType.IDENTIFIER,
|
|
128
|
+
TokenType.COMMA, TokenType.LT, TokenType.GT):
|
|
129
|
+
# Only type names and commas are valid inside <...> generics.
|
|
130
|
+
# Any operator (and, or, /, +, etc.) means this is a comparison, not generics.
|
|
131
|
+
self.pos = saved_pos
|
|
132
|
+
return False
|
|
133
|
+
self.pos += 1
|
|
134
|
+
|
|
135
|
+
self.pos = saved_pos
|
|
136
|
+
return False
|
|
137
|
+
except Exception:
|
|
138
|
+
self.pos = saved_pos
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def error(self, message: str) -> ParserError:
|
|
142
|
+
"""Create parser error at current position."""
|
|
143
|
+
token = self.current_token()
|
|
144
|
+
error = ParserError(message, token.line, token.column)
|
|
145
|
+
error.token_index = self._current_index() # read by parse_repair (card #55)
|
|
146
|
+
return error
|
|
147
|
+
|
|
148
|
+
def _current_index(self) -> int:
|
|
149
|
+
"""Index in self.tokens of current_token()."""
|
|
150
|
+
return min(self.pos, len(self.tokens) - 1)
|
|
151
|
+
|
|
152
|
+
# ========================================================================
|
|
153
|
+
# Top-level parsing
|
|
154
|
+
# ========================================================================
|
|
155
|
+
|
|
156
|
+
def parse(self) -> Script:
|
|
157
|
+
"""Parse entire Pine Script source."""
|
|
158
|
+
script = Script()
|
|
159
|
+
|
|
160
|
+
self.skip_newlines()
|
|
161
|
+
|
|
162
|
+
# Parse version annotation
|
|
163
|
+
if self.match(TokenType.VERSION_ANNOTATION):
|
|
164
|
+
version_token = self.advance()
|
|
165
|
+
script.version = VersionAnnotation(version=version_token.value)
|
|
166
|
+
self.skip_newlines()
|
|
167
|
+
|
|
168
|
+
# Parse script declaration (indicator/strategy/library)
|
|
169
|
+
if self._at_script_declaration(script.version):
|
|
170
|
+
script.script_decl = self.parse_script_declaration(script.version)
|
|
171
|
+
self.skip_newlines()
|
|
172
|
+
|
|
173
|
+
# Parse imports
|
|
174
|
+
while self.match(TokenType.IMPORT):
|
|
175
|
+
script.imports.append(self.parse_import())
|
|
176
|
+
self.skip_newlines()
|
|
177
|
+
|
|
178
|
+
# Parse global declarations and body
|
|
179
|
+
# Track actual source line numbers for ordering and blank line detection
|
|
180
|
+
while not self.match(TokenType.EOF):
|
|
181
|
+
self.skip_newlines()
|
|
182
|
+
|
|
183
|
+
if self.match(TokenType.EOF):
|
|
184
|
+
break
|
|
185
|
+
|
|
186
|
+
# Handle VERSION_ANNOTATION tokens that appear in the body
|
|
187
|
+
# (e.g., when pre-version declarations like DEFAULT_PYRAMIDING = 6
|
|
188
|
+
# appear before //@version=6 in the source)
|
|
189
|
+
if self.match(TokenType.VERSION_ANNOTATION):
|
|
190
|
+
if not script.version:
|
|
191
|
+
version_token = self.advance()
|
|
192
|
+
script.version = VersionAnnotation(version=version_token.value)
|
|
193
|
+
else:
|
|
194
|
+
self.advance() # Skip duplicate version annotations
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# Handle COMPILER_ANNOTATION tokens in the body
|
|
198
|
+
if self.match(TokenType.COMPILER_ANNOTATION):
|
|
199
|
+
self.advance() # Skip compiler annotations
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
# Handle strategy/indicator/library declarations that appear after
|
|
203
|
+
# pre-declaration code (when the initial top-level check missed them)
|
|
204
|
+
if not script.script_decl and self._at_script_declaration(script.version):
|
|
205
|
+
script.script_decl = self.parse_script_declaration(script.version)
|
|
206
|
+
self.skip_newlines()
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
# Record the starting position of each declaration/statement
|
|
210
|
+
start_token = self.current_token()
|
|
211
|
+
|
|
212
|
+
# `export type T` / `export enum E` (a library's API): parsed as the type or enum
|
|
213
|
+
if self.match(TokenType.EXPORT) and self.peek_token().type in (TokenType.TYPE, TokenType.ENUM):
|
|
214
|
+
self.advance()
|
|
215
|
+
|
|
216
|
+
# Check for function declarations
|
|
217
|
+
if self.is_function_declaration():
|
|
218
|
+
node = self.parse_function_declaration()
|
|
219
|
+
self._stamp_top_level(node, start_token)
|
|
220
|
+
script.declarations.append(node)
|
|
221
|
+
# Check for var/varip declarations
|
|
222
|
+
elif self.match(TokenType.VAR, TokenType.VARIP):
|
|
223
|
+
node = self.parse_var_declaration()
|
|
224
|
+
self._stamp_top_level(node, start_token)
|
|
225
|
+
script.declarations.append(node)
|
|
226
|
+
# Check for type declarations
|
|
227
|
+
elif self.match(TokenType.TYPE):
|
|
228
|
+
node = self.parse_type_declaration()
|
|
229
|
+
self._stamp_top_level(node, start_token)
|
|
230
|
+
script.declarations.append(node)
|
|
231
|
+
# Check for enum declarations
|
|
232
|
+
elif self.match(TokenType.ENUM):
|
|
233
|
+
node = self.parse_enum_declaration()
|
|
234
|
+
self._stamp_top_level(node, start_token)
|
|
235
|
+
script.declarations.append(node)
|
|
236
|
+
# Handle import statements that appear in the body (not just at top)
|
|
237
|
+
elif self.match(TokenType.IMPORT):
|
|
238
|
+
import_node = self.parse_import()
|
|
239
|
+
script.imports.append(import_node)
|
|
240
|
+
# Check for input declarations
|
|
241
|
+
elif self.is_input_declaration():
|
|
242
|
+
saved_pos = self.pos
|
|
243
|
+
try:
|
|
244
|
+
node = self.parse_input_declaration()
|
|
245
|
+
self._stamp_top_level(node, start_token)
|
|
246
|
+
script.declarations.append(node)
|
|
247
|
+
except Exception:
|
|
248
|
+
# Not a simple input declaration (e.g., input.string(...) == "On")
|
|
249
|
+
# Fall back to parsing as a regular statement
|
|
250
|
+
self.pos = saved_pos
|
|
251
|
+
for stmt in _as_statement_list(self.parse_statement()):
|
|
252
|
+
self._stamp_top_level(stmt, start_token)
|
|
253
|
+
script.body.append(stmt)
|
|
254
|
+
# Otherwise, parse as statement
|
|
255
|
+
else:
|
|
256
|
+
for stmt in _as_statement_list(self.parse_statement()):
|
|
257
|
+
self._stamp_top_level(stmt, start_token)
|
|
258
|
+
script.body.append(stmt)
|
|
259
|
+
|
|
260
|
+
self.skip_newlines()
|
|
261
|
+
|
|
262
|
+
script = lower_once_structures(script)
|
|
263
|
+
return lower_strategy_when_arguments(script, _pine_version(script.version))
|
|
264
|
+
|
|
265
|
+
def _at_script_declaration(self, version: Optional[VersionAnnotation]) -> bool:
|
|
266
|
+
"""At an indicator/strategy/library call; before v5 a study() call is the indicator (card #55)."""
|
|
267
|
+
if not self.match(TokenType.IDENTIFIER):
|
|
268
|
+
return False
|
|
269
|
+
name = self.current_token().value
|
|
270
|
+
if name == 'study':
|
|
271
|
+
return _pine_version(version) < 5 and self.peek_token().type == TokenType.LPAREN
|
|
272
|
+
return name in ('indicator', 'strategy', 'library')
|
|
273
|
+
|
|
274
|
+
def parse_script_declaration(self, version: Optional[VersionAnnotation] = None
|
|
275
|
+
) -> Union[IndicatorDecl, StrategyDecl, LibraryDecl]:
|
|
276
|
+
"""Parse indicator/strategy/library declaration, binding positionals to Pine's names.
|
|
277
|
+
|
|
278
|
+
`title` holds a string-literal title; any other title expression stays in kwargs
|
|
279
|
+
under 'title' and `title` is None (card #55).
|
|
280
|
+
"""
|
|
281
|
+
name_token = self.advance() # indicator, strategy, library, or v4 study
|
|
282
|
+
kind = 'indicator' if name_token.value == 'study' else name_token.value
|
|
283
|
+
self.expect(TokenType.LPAREN)
|
|
284
|
+
positionals, keywords = self._parse_declaration_arguments(name_token.value)
|
|
285
|
+
self.expect(TokenType.RPAREN)
|
|
286
|
+
kwargs = self._bind_declaration_positionals(kind, name_token, positionals, keywords, version)
|
|
287
|
+
|
|
288
|
+
title_value = kwargs.pop('title', None)
|
|
289
|
+
title_double_quoted = isinstance(title_value, Literal) and title_value.is_double_quoted
|
|
290
|
+
if title_value is None:
|
|
291
|
+
title = f"Untitled {name_token.value}"
|
|
292
|
+
elif isinstance(title_value, Literal) and title_value.literal_type == 'string':
|
|
293
|
+
title = title_value.value
|
|
294
|
+
else:
|
|
295
|
+
title = None
|
|
296
|
+
kwargs = {'title': title_value, **kwargs}
|
|
297
|
+
|
|
298
|
+
declaration_class = {'indicator': IndicatorDecl, 'strategy': StrategyDecl}.get(kind, LibraryDecl)
|
|
299
|
+
# a codegen refusal reports the declaration's line (card #40)
|
|
300
|
+
return declaration_class(title=title, kwargs=kwargs, line=name_token.line,
|
|
301
|
+
positional_count=len(positionals), title_double_quoted=title_double_quoted,
|
|
302
|
+
keyword_order=tuple(keywords))
|
|
303
|
+
|
|
304
|
+
def _parse_declaration_arguments(self, function_name: str) -> tuple:
|
|
305
|
+
"""Return (positionals, keywords) up to the closing paren.
|
|
306
|
+
|
|
307
|
+
A positional after a keyword is parsed and dropped with a warning, as before
|
|
308
|
+
card #55: its Pine parameter is not known (TradingView's reading is unmeasured).
|
|
309
|
+
"""
|
|
310
|
+
positionals, keywords = [], {}
|
|
311
|
+
while not self.match(TokenType.RPAREN):
|
|
312
|
+
self.skip_newlines() # Handle newlines before parameters
|
|
313
|
+
if self.match(TokenType.RPAREN): # Check again after skipping newlines
|
|
314
|
+
break
|
|
315
|
+
if self.match(TokenType.COMMA):
|
|
316
|
+
self.advance()
|
|
317
|
+
self.skip_newlines() # Handle newlines after comma
|
|
318
|
+
continue
|
|
319
|
+
if (self.match(TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER) and
|
|
320
|
+
self.peek_token().type == TokenType.ASSIGN):
|
|
321
|
+
key_token = self.advance()
|
|
322
|
+
if key_token.value in keywords: # published scripts do this; the last value wins, as before
|
|
323
|
+
warnings.warn(f"{key_token.line}:{key_token.column}: {function_name}() argument "
|
|
324
|
+
f"'{key_token.value}' is given twice; the last value is kept")
|
|
325
|
+
self.expect(TokenType.ASSIGN)
|
|
326
|
+
keywords[key_token.value] = self.parse_expression()
|
|
327
|
+
elif keywords:
|
|
328
|
+
token = self.current_token()
|
|
329
|
+
self.parse_expression()
|
|
330
|
+
warnings.warn(f"{token.line}:{token.column}: {function_name}() positional argument after "
|
|
331
|
+
f"a keyword argument is dropped")
|
|
332
|
+
else:
|
|
333
|
+
positionals.append(self.parse_expression())
|
|
334
|
+
return positionals, keywords
|
|
335
|
+
|
|
336
|
+
def _bind_declaration_positionals(self, kind: str, name_token: Token, positionals: list,
|
|
337
|
+
keywords: dict, version: Optional[VersionAnnotation]) -> dict:
|
|
338
|
+
"""Name each positional by Pine's parameter order; return them, then the keywords."""
|
|
339
|
+
from .pine_builtins import SCRIPT_DECLARATION_POSITIONAL_PARAMS, VERSION_INVARIANT_POSITIONAL_COUNT
|
|
340
|
+
names = SCRIPT_DECLARATION_POSITIONAL_PARAMS[kind]
|
|
341
|
+
invariant_count = VERSION_INVARIANT_POSITIONAL_COUNT[kind]
|
|
342
|
+
function_name = name_token.value
|
|
343
|
+
if len(positionals) > len(names):
|
|
344
|
+
raise self._declaration_error(
|
|
345
|
+
f"{function_name}() takes at most {len(names)} positional arguments, got {len(positionals)}",
|
|
346
|
+
name_token)
|
|
347
|
+
if len(positionals) > invariant_count and _pine_version(version) < 5:
|
|
348
|
+
raise self._declaration_error(
|
|
349
|
+
f"{function_name}() positional argument {invariant_count + 1} ('{names[invariant_count]}') "
|
|
350
|
+
f"depends on the Pine version; pass it by keyword or declare //@version=5 or later",
|
|
351
|
+
name_token)
|
|
352
|
+
if function_name == 'study':
|
|
353
|
+
keywords = self._study_keywords_as_indicator(keywords, name_token)
|
|
354
|
+
bound = dict(zip(names, positionals))
|
|
355
|
+
bound_twice = [name for name in bound if name in keywords]
|
|
356
|
+
if bound_twice:
|
|
357
|
+
raise self._declaration_error(
|
|
358
|
+
f"{function_name}() got multiple values for argument '{bound_twice[0]}'", name_token)
|
|
359
|
+
return {**bound, **keywords}
|
|
360
|
+
|
|
361
|
+
def _study_keywords_as_indicator(self, keywords: dict, name_token: Token) -> dict:
|
|
362
|
+
"""v4 study() keywords under indicator()'s names; one indicator() lacks is refused."""
|
|
363
|
+
from .pine_builtins import SCRIPT_DECLARATION_POSITIONAL_PARAMS, STUDY_KEYWORD_RENAMES
|
|
364
|
+
renamed = {STUDY_KEYWORD_RENAMES.get(key, key): value for key, value in keywords.items()}
|
|
365
|
+
unknown = [key for key in renamed if key not in SCRIPT_DECLARATION_POSITIONAL_PARAMS['indicator']]
|
|
366
|
+
if unknown:
|
|
367
|
+
raise self._declaration_error(f"study() argument '{unknown[0]}' has no indicator() equivalent",
|
|
368
|
+
name_token)
|
|
369
|
+
return renamed
|
|
370
|
+
|
|
371
|
+
def _declaration_error(self, message: str, token: Token) -> ParserError:
|
|
372
|
+
"""A declaration binding error at `token`."""
|
|
373
|
+
error = ParserError(message, token.line, token.column)
|
|
374
|
+
error.token_index = next(index for index, candidate in enumerate(self.tokens) if candidate is token)
|
|
375
|
+
return error
|
|
376
|
+
|
|
377
|
+
def parse_import(self) -> ImportDecl:
|
|
378
|
+
"""Parse import statement: import user/library/version as alias"""
|
|
379
|
+
import_token = self.expect(TokenType.IMPORT)
|
|
380
|
+
|
|
381
|
+
# Parse user/library/version as separate tokens joined by DIV (/)
|
|
382
|
+
# e.g., doqkhanh/tafirstlib/2 → IDENTIFIER DIV IDENTIFIER DIV INT_LITERAL
|
|
383
|
+
user = self.expect(TokenType.IDENTIFIER).value
|
|
384
|
+
library = ''
|
|
385
|
+
version = None
|
|
386
|
+
|
|
387
|
+
if self.match(TokenType.DIV):
|
|
388
|
+
self.advance() # consume /
|
|
389
|
+
library = self.expect(TokenType.IDENTIFIER).value
|
|
390
|
+
|
|
391
|
+
if self.match(TokenType.DIV):
|
|
392
|
+
self.advance() # consume /
|
|
393
|
+
# Version can be an integer or identifier
|
|
394
|
+
if self.match(TokenType.INT_LITERAL):
|
|
395
|
+
version = self.advance().value
|
|
396
|
+
elif self.match(TokenType.IDENTIFIER):
|
|
397
|
+
version = self.advance().value
|
|
398
|
+
|
|
399
|
+
# Parse optional 'as alias'
|
|
400
|
+
alias = None
|
|
401
|
+
if self.match(TokenType.IDENTIFIER) and self.current_token().value == 'as':
|
|
402
|
+
self.advance()
|
|
403
|
+
alias = self.expect(TokenType.IDENTIFIER).value
|
|
404
|
+
|
|
405
|
+
return ImportDecl(user=user, library=library, version=version, alias=alias, line=import_token.line)
|
|
406
|
+
|
|
407
|
+
def is_function_declaration(self) -> bool:
|
|
408
|
+
"""Check if current position is a function declaration."""
|
|
409
|
+
# Look for: [export] [method] name(params) =>
|
|
410
|
+
saved_pos = self.pos
|
|
411
|
+
|
|
412
|
+
# Skip export/method keywords
|
|
413
|
+
if self.match(TokenType.EXPORT):
|
|
414
|
+
self.pos += 1
|
|
415
|
+
if self.match(TokenType.METHOD):
|
|
416
|
+
self.pos += 1
|
|
417
|
+
|
|
418
|
+
# Check for identifier followed by ( and eventually =>; `function` is a valid name (`export function (...)`)
|
|
419
|
+
if not (self.match(TokenType.IDENTIFIER, TokenType.FUNCTION) and self.peek_token().type == TokenType.LPAREN):
|
|
420
|
+
self.pos = saved_pos
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
# Scan ahead to find => arrow (function declarations must have this)
|
|
424
|
+
# Skip past the parameter list
|
|
425
|
+
paren_depth = 0
|
|
426
|
+
while self.pos < len(self.tokens):
|
|
427
|
+
token = self.tokens[self.pos]
|
|
428
|
+
if token.type == TokenType.LPAREN:
|
|
429
|
+
paren_depth += 1
|
|
430
|
+
elif token.type == TokenType.RPAREN:
|
|
431
|
+
paren_depth -= 1
|
|
432
|
+
if paren_depth == 0:
|
|
433
|
+
# Found closing paren, check next token for =>
|
|
434
|
+
self.pos += 1
|
|
435
|
+
if self.pos < len(self.tokens) and self.tokens[self.pos].type == TokenType.ARROW:
|
|
436
|
+
self.pos = saved_pos
|
|
437
|
+
return True
|
|
438
|
+
else:
|
|
439
|
+
self.pos = saved_pos
|
|
440
|
+
return False
|
|
441
|
+
self.pos += 1
|
|
442
|
+
|
|
443
|
+
self.pos = saved_pos
|
|
444
|
+
return False
|
|
445
|
+
|
|
446
|
+
def parse_function_declaration(self) -> FuncDecl:
|
|
447
|
+
"""Parse function declaration."""
|
|
448
|
+
is_export = False
|
|
449
|
+
is_method = False
|
|
450
|
+
|
|
451
|
+
if self.match(TokenType.EXPORT):
|
|
452
|
+
is_export = True
|
|
453
|
+
self.advance()
|
|
454
|
+
|
|
455
|
+
if self.match(TokenType.METHOD):
|
|
456
|
+
is_method = True
|
|
457
|
+
self.advance()
|
|
458
|
+
|
|
459
|
+
name = (self.advance().value if self.match(TokenType.FUNCTION)
|
|
460
|
+
else self.expect(TokenType.IDENTIFIER).value)
|
|
461
|
+
|
|
462
|
+
# Parse parameters
|
|
463
|
+
self.expect(TokenType.LPAREN)
|
|
464
|
+
self.skip_newlines() # Skip newlines after opening paren
|
|
465
|
+
params = []
|
|
466
|
+
|
|
467
|
+
while not self.match(TokenType.RPAREN):
|
|
468
|
+
self.skip_newlines() # Skip newlines before each parameter
|
|
469
|
+
|
|
470
|
+
if self.match(TokenType.RPAREN):
|
|
471
|
+
break
|
|
472
|
+
|
|
473
|
+
if self.match(TokenType.COMMA):
|
|
474
|
+
self.advance()
|
|
475
|
+
continue
|
|
476
|
+
|
|
477
|
+
# Parse parameter: [type] name [= default]
|
|
478
|
+
# Type can be: simple (float), generic (map<K,V>), or array (int[]), or custom type (TradeInfo)
|
|
479
|
+
is_series = False
|
|
480
|
+
type_hint = None
|
|
481
|
+
if self.match(TokenType.TYPE_IDENTIFIER):
|
|
482
|
+
# Look ahead to disambiguate type hint vs param name:
|
|
483
|
+
# TYPE_IDENTIFIER + IDENTIFIER → type hint (e.g. "float x")
|
|
484
|
+
# TYPE_IDENTIFIER + LT → generic type hint (e.g. "map<string, int> this")
|
|
485
|
+
# TYPE_IDENTIFIER + RPAREN/COMMA/ASSIGN → param name (e.g. "array" as name)
|
|
486
|
+
next_tok = self.peek_token()
|
|
487
|
+
if next_tok.type in (TokenType.IDENTIFIER, TokenType.LT, TokenType.TYPE_IDENTIFIER) \
|
|
488
|
+
or self._array_type_ahead():
|
|
489
|
+
is_series = self.current_token().value == 'series' and (
|
|
490
|
+
next_tok.type == TokenType.TYPE_IDENTIFIER
|
|
491
|
+
or (next_tok.type == TokenType.IDENTIFIER
|
|
492
|
+
and self.peek_token(2).type in (TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER)))
|
|
493
|
+
type_hint = self.parse_generic_type()
|
|
494
|
+
# else: fall through, treat TYPE_IDENTIFIER as param name
|
|
495
|
+
elif self.match(TokenType.IDENTIFIER) and self._array_type_ahead():
|
|
496
|
+
# `FVG[] arr`: an array of a user type
|
|
497
|
+
element = self.advance().value
|
|
498
|
+
self.advance() # [
|
|
499
|
+
self.advance() # ]
|
|
500
|
+
type_hint = f'array<{element}>'
|
|
501
|
+
elif self.match(TokenType.IDENTIFIER) and self.peek_token().type == TokenType.IDENTIFIER:
|
|
502
|
+
# For method parameters like "TradeInfo this" or "int val"
|
|
503
|
+
# First IDENTIFIER is the type, second is the parameter name
|
|
504
|
+
type_hint = self.advance().value
|
|
505
|
+
|
|
506
|
+
# Param name can be IDENTIFIER or TYPE_IDENTIFIER used as a name
|
|
507
|
+
if self.match(TokenType.IDENTIFIER):
|
|
508
|
+
param_name = self.advance().value
|
|
509
|
+
elif self.match(TokenType.TYPE_IDENTIFIER):
|
|
510
|
+
param_name = self.advance().value
|
|
511
|
+
else:
|
|
512
|
+
param_name = self.expect(TokenType.IDENTIFIER).value
|
|
513
|
+
|
|
514
|
+
default = None
|
|
515
|
+
if self.match(TokenType.ASSIGN):
|
|
516
|
+
self.advance()
|
|
517
|
+
default = self.parse_expression()
|
|
518
|
+
|
|
519
|
+
params.append(Parameter(name=param_name, type_hint=type_hint, default=default, is_series=is_series))
|
|
520
|
+
self.skip_newlines() # Skip newlines after parameter
|
|
521
|
+
|
|
522
|
+
self.expect(TokenType.RPAREN)
|
|
523
|
+
|
|
524
|
+
# Parse => and body
|
|
525
|
+
self.expect(TokenType.ARROW)
|
|
526
|
+
self.skip_newlines()
|
|
527
|
+
|
|
528
|
+
# Check if single-line or multi-line body
|
|
529
|
+
if self.match(TokenType.INDENT):
|
|
530
|
+
# Multi-line body
|
|
531
|
+
self.advance()
|
|
532
|
+
body = self.parse_block()
|
|
533
|
+
self.expect(TokenType.DEDENT)
|
|
534
|
+
else:
|
|
535
|
+
# Single-line body — may have comma-separated statements
|
|
536
|
+
# e.g., fn(x) => a = expr1, b = expr2, return_expr
|
|
537
|
+
body = self._parse_single_line_body()
|
|
538
|
+
|
|
539
|
+
return FuncDecl(name=name, params=params, body=body,
|
|
540
|
+
is_method=is_method, is_export=is_export)
|
|
541
|
+
|
|
542
|
+
def _parse_single_line_body(self):
|
|
543
|
+
"""Parse single-line function body with optional comma-separated statements.
|
|
544
|
+
Pine Script allows: fn(x) => stmt1, stmt2, return_expr
|
|
545
|
+
Each comma-separated item can be an assignment or expression.
|
|
546
|
+
Returns a single expression (no commas) or a list of statements (commas present).
|
|
547
|
+
"""
|
|
548
|
+
# Parse first item — could be var declaration, assignment, or expression
|
|
549
|
+
first = self._parse_positioned_single_line_item()
|
|
550
|
+
|
|
551
|
+
# Check for comma (multiple statements)
|
|
552
|
+
if not self.match(TokenType.COMMA):
|
|
553
|
+
return first # Single expression, return as-is
|
|
554
|
+
|
|
555
|
+
# Multiple comma-separated statements
|
|
556
|
+
items = [first]
|
|
557
|
+
while self.match(TokenType.COMMA):
|
|
558
|
+
self.advance() # consume comma
|
|
559
|
+
self.skip_newlines()
|
|
560
|
+
items.append(self._parse_positioned_single_line_item())
|
|
561
|
+
|
|
562
|
+
return items
|
|
563
|
+
|
|
564
|
+
def _parse_single_line_item(self):
|
|
565
|
+
"""Parse a single item in a comma-separated single-line function body.
|
|
566
|
+
Can be: var type name = expr, name = expr, or just expr.
|
|
567
|
+
"""
|
|
568
|
+
# Handle 'var' declarations: var type name = expr
|
|
569
|
+
if self.match(TokenType.VAR):
|
|
570
|
+
return self.parse_statement()
|
|
571
|
+
|
|
572
|
+
# Try assignment: name = expr (look ahead for IDENTIFIER followed by ASSIGN)
|
|
573
|
+
saved_pos = self.pos
|
|
574
|
+
if self.match(TokenType.TYPE_IDENTIFIER):
|
|
575
|
+
# Could be typed assignment: float x = expr
|
|
576
|
+
type_hint = self.parse_generic_type()
|
|
577
|
+
if self.match(TokenType.IDENTIFIER):
|
|
578
|
+
var_name = self.advance().value
|
|
579
|
+
if self.match(TokenType.ASSIGN):
|
|
580
|
+
self.advance()
|
|
581
|
+
value = self.parse_expression()
|
|
582
|
+
return Assignment(target=var_name, value=value, type_hint=type_hint)
|
|
583
|
+
self.pos = saved_pos
|
|
584
|
+
|
|
585
|
+
if self.match(TokenType.IDENTIFIER):
|
|
586
|
+
maybe_name = self.current_token().value
|
|
587
|
+
next_pos = self.pos + 1
|
|
588
|
+
if next_pos < len(self.tokens) and self.tokens[next_pos].type == TokenType.ASSIGN:
|
|
589
|
+
var_name = self.advance().value
|
|
590
|
+
self.advance() # consume =
|
|
591
|
+
value = self.parse_expression()
|
|
592
|
+
return Assignment(target=var_name, value=value)
|
|
593
|
+
# Not an assignment — fall through to expression parse
|
|
594
|
+
self.pos = saved_pos
|
|
595
|
+
|
|
596
|
+
return self.parse_expression()
|
|
597
|
+
|
|
598
|
+
def _array_type_ahead(self) -> bool:
|
|
599
|
+
"""At `T[] name`: an array type in the bracket spelling, followed by a name."""
|
|
600
|
+
return (self.peek_token().type == TokenType.LBRACKET and self.peek_token(2).type == TokenType.RBRACKET
|
|
601
|
+
and self.peek_token(3).type in (TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER))
|
|
602
|
+
|
|
603
|
+
def parse_generic_type(self) -> str:
|
|
604
|
+
"""
|
|
605
|
+
Parse type syntax:
|
|
606
|
+
- Modifier + type: simple int, series float, const string, input float
|
|
607
|
+
- Generic: type<T> or map<K, V>
|
|
608
|
+
- Array bracket: type[] (e.g., string[], int[])
|
|
609
|
+
"""
|
|
610
|
+
# Pine Script type modifiers (no Python equivalent, consume and drop)
|
|
611
|
+
TYPE_MODIFIERS = {'simple', 'series', 'const', 'input'}
|
|
612
|
+
|
|
613
|
+
base_type = self.advance().value # Get base type (map, array, string, int, etc.)
|
|
614
|
+
|
|
615
|
+
# Handle type modifiers: simple int, series float, etc.
|
|
616
|
+
# The modifier is consumed; `const` is remembered for the declaration being parsed
|
|
617
|
+
self.last_type_modifier = None
|
|
618
|
+
if base_type in TYPE_MODIFIERS and (
|
|
619
|
+
self.match(TokenType.TYPE_IDENTIFIER)
|
|
620
|
+
# `simple Timezone tz`, `series collectedData value`: a modifier before a user type
|
|
621
|
+
or (self.match(TokenType.IDENTIFIER) and self.peek_token().type in (
|
|
622
|
+
TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER, TokenType.LBRACKET))):
|
|
623
|
+
self.last_type_modifier = base_type
|
|
624
|
+
base_type = self.advance().value
|
|
625
|
+
|
|
626
|
+
# Check for array bracket syntax: type[]
|
|
627
|
+
if self.match(TokenType.LBRACKET):
|
|
628
|
+
self.advance() # consume [
|
|
629
|
+
self.expect(TokenType.RBRACKET) # consume ]
|
|
630
|
+
# Convert to generic array syntax: string[] -> array<string>
|
|
631
|
+
return f"array<{base_type}>"
|
|
632
|
+
|
|
633
|
+
# Check for generic parameters: type<T>
|
|
634
|
+
if self.match(TokenType.LT):
|
|
635
|
+
self.advance() # consume <
|
|
636
|
+
|
|
637
|
+
generic_params = []
|
|
638
|
+
while not self.match(TokenType.GT):
|
|
639
|
+
if self.match(TokenType.COMMA):
|
|
640
|
+
self.advance()
|
|
641
|
+
continue
|
|
642
|
+
|
|
643
|
+
# Parse type parameter (can be TYPE_IDENTIFIER or IDENTIFIER)
|
|
644
|
+
if self.match(TokenType.TYPE_IDENTIFIER, TokenType.IDENTIFIER):
|
|
645
|
+
generic_params.append(self.advance().value)
|
|
646
|
+
else:
|
|
647
|
+
raise self.error(f"Expected type parameter, got {self.current_token().type.name}")
|
|
648
|
+
|
|
649
|
+
self.expect(TokenType.GT) # consume >
|
|
650
|
+
|
|
651
|
+
# Format as Python generic: map<string, float> -> dict[str, float]
|
|
652
|
+
return f"{base_type}<{', '.join(generic_params)}>"
|
|
653
|
+
|
|
654
|
+
return base_type
|
|
655
|
+
|
|
656
|
+
def parse_var_declaration(self) -> Union[VarDecl, VaripDecl]:
|
|
657
|
+
"""Parse var/varip declaration."""
|
|
658
|
+
is_var = self.match(TokenType.VAR)
|
|
659
|
+
self.advance() # var or varip
|
|
660
|
+
|
|
661
|
+
# Parse optional type hint (with generic support or UDT name)
|
|
662
|
+
type_hint = None
|
|
663
|
+
# A type keyword directly followed by '=' is being used as the variable
|
|
664
|
+
# NAME, not a type hint — Pine lets a variable shadow a type namespace
|
|
665
|
+
# (e.g. `var matrix = matrix.new<float>(...)`, `var array = ...`). A real
|
|
666
|
+
# type hint is instead followed by a name or a generic '<'. Skip the
|
|
667
|
+
# type-hint branch so the keyword is consumed as the name below.
|
|
668
|
+
if self.match(TokenType.TYPE_IDENTIFIER) and self.peek_token().type != TokenType.ASSIGN:
|
|
669
|
+
type_hint = self.parse_generic_type()
|
|
670
|
+
elif self.match(TokenType.IDENTIFIER):
|
|
671
|
+
# Could be a UDT type name like Statistics, TradeInfo
|
|
672
|
+
next_type = self.peek_token().type
|
|
673
|
+
if next_type == TokenType.IDENTIFIER:
|
|
674
|
+
# Simple UDT: TradeInfo myVar = ...
|
|
675
|
+
type_hint = self.advance().value
|
|
676
|
+
elif next_type in (TokenType.LBRACKET, TokenType.LT):
|
|
677
|
+
# UDT array/generic: TestResult[] results = ..., MyType<T> x = ...
|
|
678
|
+
type_hint = self.advance().value # UDT name
|
|
679
|
+
if self.match(TokenType.LBRACKET):
|
|
680
|
+
self.advance() # consume [
|
|
681
|
+
self.expect(TokenType.RBRACKET) # consume ]
|
|
682
|
+
type_hint = f"array<{type_hint}>"
|
|
683
|
+
elif self.match(TokenType.LT):
|
|
684
|
+
# Parse generic params
|
|
685
|
+
self.advance() # consume <
|
|
686
|
+
generic_params = []
|
|
687
|
+
while not self.match(TokenType.GT):
|
|
688
|
+
if self.match(TokenType.COMMA):
|
|
689
|
+
self.advance()
|
|
690
|
+
continue
|
|
691
|
+
if self.match(TokenType.TYPE_IDENTIFIER, TokenType.IDENTIFIER):
|
|
692
|
+
generic_params.append(self.advance().value)
|
|
693
|
+
else:
|
|
694
|
+
raise self.error(f"Expected type parameter, got {self.current_token().type.name}")
|
|
695
|
+
self.expect(TokenType.GT)
|
|
696
|
+
type_hint = f"{type_hint}<{', '.join(generic_params)}>"
|
|
697
|
+
|
|
698
|
+
# Parse variable name. Accept a TYPE_IDENTIFIER here too: when no type
|
|
699
|
+
# hint was taken, a type keyword in this position is the variable name
|
|
700
|
+
# (`var matrix = ...`), which Pine permits.
|
|
701
|
+
# After a type, the name may be a type keyword too (`var color color = na`)
|
|
702
|
+
if self.match(TokenType.TYPE_IDENTIFIER) and self.peek_token().type == TokenType.ASSIGN:
|
|
703
|
+
name = self.advance().value
|
|
704
|
+
else:
|
|
705
|
+
name = self.expect(TokenType.IDENTIFIER).value
|
|
706
|
+
|
|
707
|
+
# Parse = value
|
|
708
|
+
self.expect(TokenType.ASSIGN)
|
|
709
|
+
value = self.parse_expression()
|
|
710
|
+
|
|
711
|
+
if is_var:
|
|
712
|
+
return VarDecl(name=name, value=value, type_hint=type_hint)
|
|
713
|
+
else:
|
|
714
|
+
return VaripDecl(name=name, value=value, type_hint=type_hint)
|
|
715
|
+
|
|
716
|
+
def parse_type_declaration(self) -> TypeDecl:
|
|
717
|
+
"""Parse type (UDT) declaration."""
|
|
718
|
+
self.expect(TokenType.TYPE)
|
|
719
|
+
name = self.expect(TokenType.IDENTIFIER).value
|
|
720
|
+
|
|
721
|
+
self.skip_newlines()
|
|
722
|
+
self.expect(TokenType.INDENT)
|
|
723
|
+
|
|
724
|
+
fields = []
|
|
725
|
+
field_lines = []
|
|
726
|
+
while not self.match(TokenType.DEDENT):
|
|
727
|
+
self.skip_newlines()
|
|
728
|
+
if self.match(TokenType.DEDENT):
|
|
729
|
+
break
|
|
730
|
+
field_lines.append(self.current_token().line)
|
|
731
|
+
|
|
732
|
+
# Parse field: [var|varip] type name [= default_value]
|
|
733
|
+
# Type can be a built-in type (TYPE_IDENTIFIER), generic (array<T>), or UDT (IDENTIFIER)
|
|
734
|
+
# varip behaves like var since we don't use realtime candles
|
|
735
|
+
field_is_var = False
|
|
736
|
+
if self.match(TokenType.VAR, TokenType.VARIP):
|
|
737
|
+
self.advance() # consume var/varip
|
|
738
|
+
field_is_var = True
|
|
739
|
+
if self.match(TokenType.TYPE_IDENTIFIER):
|
|
740
|
+
type_hint = self.parse_generic_type()
|
|
741
|
+
elif self.match(TokenType.IDENTIFIER):
|
|
742
|
+
type_hint = self.advance().value
|
|
743
|
+
else:
|
|
744
|
+
raise self.error(f"Expected type, got {self.current_token().type.name}")
|
|
745
|
+
# a field may be named like a type: `box box`
|
|
746
|
+
field_token = (self.advance() if self.match(TokenType.TYPE_IDENTIFIER)
|
|
747
|
+
else self.expect(TokenType.IDENTIFIER))
|
|
748
|
+
field_name = field_token.value
|
|
749
|
+
|
|
750
|
+
# Handle optional default value; it carries its field's line, so a
|
|
751
|
+
# refused default names the Pine line (card #17)
|
|
752
|
+
default_value = None
|
|
753
|
+
if self.match(TokenType.ASSIGN):
|
|
754
|
+
self.advance() # consume =
|
|
755
|
+
default_value = self.parse_expression()
|
|
756
|
+
if isinstance(default_value, ASTNode) and not default_value.line:
|
|
757
|
+
default_value = dataclasses.replace(default_value, line=field_token.line)
|
|
758
|
+
|
|
759
|
+
fields.append((field_name, type_hint, default_value))
|
|
760
|
+
self.skip_newlines()
|
|
761
|
+
|
|
762
|
+
self.expect(TokenType.DEDENT)
|
|
763
|
+
|
|
764
|
+
return TypeDecl(name=name, fields=fields, field_lines=field_lines)
|
|
765
|
+
|
|
766
|
+
def parse_enum_declaration(self) -> EnumDecl:
|
|
767
|
+
"""Parse enum declaration."""
|
|
768
|
+
self.expect(TokenType.ENUM)
|
|
769
|
+
# Enum name can be IDENTIFIER or TYPE_IDENTIFIER (e.g., "enum polyline", "enum ta")
|
|
770
|
+
if self.match(TokenType.IDENTIFIER):
|
|
771
|
+
name = self.advance().value
|
|
772
|
+
elif self.match(TokenType.TYPE_IDENTIFIER):
|
|
773
|
+
name = self.advance().value
|
|
774
|
+
else:
|
|
775
|
+
name = self.expect(TokenType.IDENTIFIER).value
|
|
776
|
+
|
|
777
|
+
self.skip_newlines()
|
|
778
|
+
self.expect(TokenType.INDENT)
|
|
779
|
+
|
|
780
|
+
members = []
|
|
781
|
+
while not self.match(TokenType.DEDENT):
|
|
782
|
+
self.skip_newlines()
|
|
783
|
+
if self.match(TokenType.DEDENT):
|
|
784
|
+
break
|
|
785
|
+
|
|
786
|
+
# Parse enum member: name [= value]; can also be TYPE_IDENTIFIER
|
|
787
|
+
if self.match(TokenType.IDENTIFIER):
|
|
788
|
+
member = self.advance().value
|
|
789
|
+
elif self.match(TokenType.TYPE_IDENTIFIER):
|
|
790
|
+
member = self.advance().value
|
|
791
|
+
else:
|
|
792
|
+
member = self.expect(TokenType.IDENTIFIER).value
|
|
793
|
+
|
|
794
|
+
# Check for optional value assignment
|
|
795
|
+
if self.match(TokenType.ASSIGN):
|
|
796
|
+
self.advance() # consume =
|
|
797
|
+
# Parse the value (can be string, int, etc.)
|
|
798
|
+
value = self.parse_expression()
|
|
799
|
+
# Store as tuple (name, value) or just name
|
|
800
|
+
members.append((member, value))
|
|
801
|
+
else:
|
|
802
|
+
members.append(member)
|
|
803
|
+
|
|
804
|
+
self.skip_newlines()
|
|
805
|
+
|
|
806
|
+
self.expect(TokenType.DEDENT)
|
|
807
|
+
|
|
808
|
+
return EnumDecl(name=name, members=members)
|
|
809
|
+
|
|
810
|
+
def is_input_declaration(self) -> bool:
|
|
811
|
+
"""Check if current position is an input declaration.
|
|
812
|
+
Matches: name = input.*() or type name = input.*()
|
|
813
|
+
"""
|
|
814
|
+
saved_pos = self.pos
|
|
815
|
+
|
|
816
|
+
# Skip optional type hint (e.g., float, int, string)
|
|
817
|
+
if self.match(TokenType.TYPE_IDENTIFIER):
|
|
818
|
+
self.pos += 1
|
|
819
|
+
|
|
820
|
+
# Must have an identifier (the variable name)
|
|
821
|
+
if not self.match(TokenType.IDENTIFIER):
|
|
822
|
+
self.pos = saved_pos
|
|
823
|
+
return False
|
|
824
|
+
|
|
825
|
+
self.pos += 1 # Skip identifier
|
|
826
|
+
|
|
827
|
+
# Check for = input.
|
|
828
|
+
result = (self.match(TokenType.ASSIGN) and
|
|
829
|
+
((self.peek_token().type == TokenType.IDENTIFIER and
|
|
830
|
+
self.peek_token().value.startswith('input.')) or
|
|
831
|
+
self._legacy_typed_input_call_ahead()))
|
|
832
|
+
|
|
833
|
+
self.pos = saved_pos
|
|
834
|
+
return result
|
|
835
|
+
|
|
836
|
+
def _legacy_typed_input_call_ahead(self) -> bool:
|
|
837
|
+
"""At `= input(` ... `type=<kind>` ... `)` with a kind parse_function_call rewrites to an input.* call
|
|
838
|
+
(card #115): such a call is an input declaration, as its v5 spelling is."""
|
|
839
|
+
from .pine_builtins import LEGACY_INPUT_TYPE_ARGUMENT_FUNCTIONS
|
|
840
|
+
if self.peek_token().value != 'input' or self.peek_token(2).type != TokenType.LPAREN:
|
|
841
|
+
return False
|
|
842
|
+
depth, offset = 0, 2
|
|
843
|
+
while True:
|
|
844
|
+
token = self.peek_token(offset)
|
|
845
|
+
if token.type == TokenType.EOF:
|
|
846
|
+
return False
|
|
847
|
+
if token.type in (TokenType.LPAREN, TokenType.LBRACKET):
|
|
848
|
+
depth += 1
|
|
849
|
+
elif token.type in (TokenType.RPAREN, TokenType.RBRACKET):
|
|
850
|
+
depth -= 1
|
|
851
|
+
if depth == 0:
|
|
852
|
+
return False
|
|
853
|
+
elif (depth == 1 and token.type == TokenType.IDENTIFIER and token.value == 'type'
|
|
854
|
+
and self.peek_token(offset + 1).type == TokenType.ASSIGN):
|
|
855
|
+
kind = self.peek_token(offset + 2)
|
|
856
|
+
# a bare `float`/`bool`/`string`/`color` lexes as TYPE_IDENTIFIER; parse_function_call rewrites it too
|
|
857
|
+
return (kind.type in (TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER)
|
|
858
|
+
and kind.value.removeprefix('input.') in LEGACY_INPUT_TYPE_ARGUMENT_FUNCTIONS)
|
|
859
|
+
offset += 1
|
|
860
|
+
|
|
861
|
+
def parse_input_declaration(self) -> InputDecl:
|
|
862
|
+
"""Parse input declaration. Handles: name = input.*() and type name = input.*()"""
|
|
863
|
+
# Skip optional type hint
|
|
864
|
+
if self.match(TokenType.TYPE_IDENTIFIER):
|
|
865
|
+
self.advance()
|
|
866
|
+
|
|
867
|
+
name = self.advance().value
|
|
868
|
+
self.expect(TokenType.ASSIGN)
|
|
869
|
+
|
|
870
|
+
# Parse input.*() call
|
|
871
|
+
func_call = self.parse_expression()
|
|
872
|
+
|
|
873
|
+
if not isinstance(func_call, FunctionCall):
|
|
874
|
+
raise self.error("Expected input function call")
|
|
875
|
+
|
|
876
|
+
# Extract function name and arguments
|
|
877
|
+
if isinstance(func_call.func, str):
|
|
878
|
+
func = func_call.func
|
|
879
|
+
elif isinstance(func_call.func, MemberAccess):
|
|
880
|
+
func = f"{func_call.func.object}.{func_call.func.member}"
|
|
881
|
+
else:
|
|
882
|
+
func = "input"
|
|
883
|
+
|
|
884
|
+
return InputDecl(name=name, func=func, args=func_call.args, kwargs=func_call.kwargs)
|
|
885
|
+
|
|
886
|
+
# ========================================================================
|
|
887
|
+
# Statement parsing
|
|
888
|
+
# ========================================================================
|
|
889
|
+
|
|
890
|
+
def parse_statement(self) -> Union[Statement, List[Statement], None]:
|
|
891
|
+
"""Parse one source statement: a statement, a list of statements when the source statement is
|
|
892
|
+
lowered to several (an assignment to a field of a call result, card #98), or None."""
|
|
893
|
+
self.skip_newlines()
|
|
894
|
+
|
|
895
|
+
# If statement
|
|
896
|
+
if self.match(TokenType.IF):
|
|
897
|
+
return self.parse_if_statement()
|
|
898
|
+
|
|
899
|
+
# For loop
|
|
900
|
+
if self.match(TokenType.FOR):
|
|
901
|
+
return self.parse_for_loop()
|
|
902
|
+
|
|
903
|
+
# While loop
|
|
904
|
+
if self.match(TokenType.WHILE):
|
|
905
|
+
return self.parse_while_loop()
|
|
906
|
+
|
|
907
|
+
# Switch statement
|
|
908
|
+
if self.match(TokenType.SWITCH):
|
|
909
|
+
return self.parse_switch_statement()
|
|
910
|
+
|
|
911
|
+
# Break
|
|
912
|
+
if self.match(TokenType.BREAK):
|
|
913
|
+
self.advance()
|
|
914
|
+
return BreakStatement()
|
|
915
|
+
|
|
916
|
+
# Continue
|
|
917
|
+
if self.match(TokenType.CONTINUE):
|
|
918
|
+
self.advance()
|
|
919
|
+
return ContinueStatement()
|
|
920
|
+
|
|
921
|
+
# once structure (Pine v6): `once [condition]` + an indented block (card #59)
|
|
922
|
+
if self._at_once_structure():
|
|
923
|
+
return self.parse_once_statement()
|
|
924
|
+
|
|
925
|
+
# Var/varip declarations (can appear inside blocks in Pine Script v6)
|
|
926
|
+
if self.match(TokenType.VAR, TokenType.VARIP):
|
|
927
|
+
return self.parse_var_declaration()
|
|
928
|
+
|
|
929
|
+
# Local variable declaration with explicit type: type name = value
|
|
930
|
+
# Examples: float x = 10.0, string[] ids = array.new_string()
|
|
931
|
+
# Also handles user-defined types: Signal signalState = ...
|
|
932
|
+
# A leading type keyword is only a declaration when followed by the var
|
|
933
|
+
# NAME (IDENTIFIER), a generic '<', or an array '[]'. Anything else means
|
|
934
|
+
# the keyword is being used as a VALUE — a type-conversion call
|
|
935
|
+
# `bool(na)` (LPAREN), a bare variable reference `matrix` (NEWLINE, when a
|
|
936
|
+
# variable shadows the type namespace), member access `matrix.set(...)`
|
|
937
|
+
# (DOT), or history access `matrix[0]` — all of which are expressions.
|
|
938
|
+
# A following TYPE_IDENTIFIER also means a decl — a qualifier/modifier
|
|
939
|
+
# chain such as `const matrix<T> m`, `series int x`, `simple string s`.
|
|
940
|
+
_pk = self.peek_token().type
|
|
941
|
+
_is_typed_decl = _pk in (TokenType.LT, TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER) or (
|
|
942
|
+
_pk == TokenType.LBRACKET
|
|
943
|
+
and self.pos + 2 < len(self.tokens)
|
|
944
|
+
and self.tokens[self.pos + 2].type == TokenType.RBRACKET
|
|
945
|
+
)
|
|
946
|
+
if self.match(TokenType.TYPE_IDENTIFIER) and _is_typed_decl:
|
|
947
|
+
type_hint = self.parse_generic_type()
|
|
948
|
+
is_const = getattr(self, 'last_type_modifier', None) == 'const'
|
|
949
|
+
var_name = (self.advance().value if self.match(TokenType.TYPE_IDENTIFIER)
|
|
950
|
+
and self.peek_token().type == TokenType.ASSIGN
|
|
951
|
+
else self.expect(TokenType.IDENTIFIER).value)
|
|
952
|
+
self.expect(TokenType.ASSIGN)
|
|
953
|
+
value = self.parse_expression()
|
|
954
|
+
# Return as a local variable assignment with type hint
|
|
955
|
+
return Assignment(target=var_name, value=value, type_hint=type_hint, is_const=is_const)
|
|
956
|
+
|
|
957
|
+
# Check for user-defined type declaration: UDT varName = value
|
|
958
|
+
# Heuristic: IDENTIFIER IDENTIFIER ASSIGN (e.g., Signal signalState =)
|
|
959
|
+
if (self.match(TokenType.IDENTIFIER) and
|
|
960
|
+
self.pos + 1 < len(self.tokens) and
|
|
961
|
+
self.tokens[self.pos + 1].type == TokenType.IDENTIFIER and
|
|
962
|
+
self.pos + 2 < len(self.tokens) and
|
|
963
|
+
self.tokens[self.pos + 2].type == TokenType.ASSIGN):
|
|
964
|
+
type_name = self.advance().value
|
|
965
|
+
var_name = self.advance().value
|
|
966
|
+
self.expect(TokenType.ASSIGN)
|
|
967
|
+
value = self.parse_expression()
|
|
968
|
+
return Assignment(target=var_name, value=value, type_hint=type_name)
|
|
969
|
+
|
|
970
|
+
# Check for UDT array declaration: UDT[] varName = value
|
|
971
|
+
# Heuristic: IDENTIFIER LBRACKET RBRACKET IDENTIFIER ASSIGN
|
|
972
|
+
if (self.match(TokenType.IDENTIFIER) and
|
|
973
|
+
self.pos + 1 < len(self.tokens) and
|
|
974
|
+
self.tokens[self.pos + 1].type == TokenType.LBRACKET and
|
|
975
|
+
self.pos + 2 < len(self.tokens) and
|
|
976
|
+
self.tokens[self.pos + 2].type == TokenType.RBRACKET and
|
|
977
|
+
self.pos + 3 < len(self.tokens) and
|
|
978
|
+
self.tokens[self.pos + 3].type == TokenType.IDENTIFIER and
|
|
979
|
+
self.pos + 4 < len(self.tokens) and
|
|
980
|
+
self.tokens[self.pos + 4].type == TokenType.ASSIGN):
|
|
981
|
+
udt_name = self.advance().value
|
|
982
|
+
self.advance() # consume [
|
|
983
|
+
self.advance() # consume ]
|
|
984
|
+
var_name = self.advance().value
|
|
985
|
+
self.expect(TokenType.ASSIGN)
|
|
986
|
+
value = self.parse_expression()
|
|
987
|
+
return Assignment(target=var_name, value=value, type_hint=f"array<{udt_name}>")
|
|
988
|
+
|
|
989
|
+
# Check for nested function declarations: name(params) =>
|
|
990
|
+
if self.is_function_declaration():
|
|
991
|
+
return self.parse_function_declaration()
|
|
992
|
+
|
|
993
|
+
# Assignment or expression statement
|
|
994
|
+
start_token = self.current_token()
|
|
995
|
+
expr = self.parse_expression()
|
|
996
|
+
|
|
997
|
+
# Check for assignment/reassignment
|
|
998
|
+
if self.match(TokenType.ASSIGN):
|
|
999
|
+
operator_token = self.advance()
|
|
1000
|
+
self._refuse_history_target(expr, operator_token)
|
|
1001
|
+
call_result_field = self._is_call_result_field(expr)
|
|
1002
|
+
if not call_result_field:
|
|
1003
|
+
self._refuse_unassignable_target(expr, operator_token, tuple_allowed=True)
|
|
1004
|
+
value = self.parse_expression()
|
|
1005
|
+
|
|
1006
|
+
if call_result_field:
|
|
1007
|
+
return self._lower_call_result_field_assignment(expr, operator_token, value, start_token)
|
|
1008
|
+
if isinstance(expr, Identifier):
|
|
1009
|
+
return Assignment(target=expr.name, value=value)
|
|
1010
|
+
elif isinstance(expr, ArrayLiteral):
|
|
1011
|
+
# Tuple destructuring
|
|
1012
|
+
names = [e.name if isinstance(e, Identifier) else str(_without_positions(e)) for e in expr.elements]
|
|
1013
|
+
return Assignment(target=TupleDestructure(names=names, value=value), value=value)
|
|
1014
|
+
|
|
1015
|
+
elif self.match(TokenType.REASSIGN):
|
|
1016
|
+
operator_token = self.advance()
|
|
1017
|
+
self._refuse_history_target(expr, operator_token)
|
|
1018
|
+
call_result_field = self._is_call_result_field(expr)
|
|
1019
|
+
if not call_result_field:
|
|
1020
|
+
self._refuse_unassignable_target(expr, operator_token)
|
|
1021
|
+
value = self.parse_expression()
|
|
1022
|
+
|
|
1023
|
+
if call_result_field:
|
|
1024
|
+
return self._lower_call_result_field_assignment(expr, operator_token, value, start_token)
|
|
1025
|
+
if isinstance(expr, Identifier):
|
|
1026
|
+
return Reassignment(target=expr.name, value=value)
|
|
1027
|
+
|
|
1028
|
+
# Compound assignment operators: +=, -=, *=, /=, %=
|
|
1029
|
+
elif self.match(TokenType.PLUS_ASSIGN, TokenType.MINUS_ASSIGN, TokenType.MULT_ASSIGN,
|
|
1030
|
+
TokenType.DIV_ASSIGN, TokenType.MOD_ASSIGN):
|
|
1031
|
+
op_token = self.advance()
|
|
1032
|
+
self._refuse_history_target(expr, op_token)
|
|
1033
|
+
call_result_field = self._is_call_result_field(expr)
|
|
1034
|
+
if not call_result_field:
|
|
1035
|
+
self._refuse_unassignable_target(expr, op_token)
|
|
1036
|
+
right_value = self.parse_expression()
|
|
1037
|
+
|
|
1038
|
+
if call_result_field:
|
|
1039
|
+
return self._lower_call_result_field_assignment(expr, op_token, right_value, start_token)
|
|
1040
|
+
if isinstance(expr, Identifier):
|
|
1041
|
+
# Convert x += 5 to x := x + 5
|
|
1042
|
+
op_symbol = COMPOUND_ASSIGNMENT_OPERATORS[op_token.type]
|
|
1043
|
+
|
|
1044
|
+
# Create binary expression: x + 5
|
|
1045
|
+
from .ast_nodes import BinaryOp
|
|
1046
|
+
new_value = BinaryOp(left=expr, op=op_symbol, right=right_value, compound=True)
|
|
1047
|
+
|
|
1048
|
+
# Return reassignment: x := (x + 5)
|
|
1049
|
+
return Reassignment(target=expr.name, value=new_value)
|
|
1050
|
+
|
|
1051
|
+
# Expression statement
|
|
1052
|
+
return ExpressionStatement(expr=expr)
|
|
1053
|
+
|
|
1054
|
+
@staticmethod
|
|
1055
|
+
def _refuse_history_target(target: Expression, operator_token: Token) -> None:
|
|
1056
|
+
"""`x[1] := v`, `x[1] += v`, `x[1] = v`: TradingView refuses an
|
|
1057
|
+
assignment to a history reference. The tree has no node that could keep
|
|
1058
|
+
it (the statement used to be dropped), so it is refused here, at the
|
|
1059
|
+
operator (card #28)."""
|
|
1060
|
+
if isinstance(target, IndexAccess):
|
|
1061
|
+
raise SemanticError('history-target',
|
|
1062
|
+
f'cannot assign with {operator_token.value!r} to a history reference (x[n])',
|
|
1063
|
+
operator_token.line, operator_token.column)
|
|
1064
|
+
|
|
1065
|
+
@staticmethod
|
|
1066
|
+
def _is_call_result_field(target: Expression) -> bool:
|
|
1067
|
+
"""`arr.get(0).v`: a field of the value an expression returns (card #98)."""
|
|
1068
|
+
return isinstance(target, MemberAccess) and not isinstance(target.object, str)
|
|
1069
|
+
|
|
1070
|
+
def _lower_call_result_field_assignment(self, target: MemberAccess, operator_token: Token, value: Expression,
|
|
1071
|
+
start_token: Token) -> List[Statement]:
|
|
1072
|
+
"""`arr.get(0).v += 1` -> `assigned_field_receiver1 = arr.get(0)` then
|
|
1073
|
+
`assigned_field_receiver1.v := assigned_field_receiver1.v + 1` (card #98).
|
|
1074
|
+
|
|
1075
|
+
The receiver is the object a call returns, so the field changes in that object; binding it
|
|
1076
|
+
to a variable first evaluates the call once, as Pine does, and uses the dotted-name
|
|
1077
|
+
assignment every later stage already handles. The variable is fresh against every
|
|
1078
|
+
identifier of the script."""
|
|
1079
|
+
receiver = self._fresh_receiver_name()
|
|
1080
|
+
field = f'{receiver}.{target.member}'
|
|
1081
|
+
position = self._position_of(start_token)
|
|
1082
|
+
if operator_token.type is TokenType.ASSIGN:
|
|
1083
|
+
assigned = Assignment(target=field, value=value)
|
|
1084
|
+
elif operator_token.type is TokenType.REASSIGN:
|
|
1085
|
+
assigned = Reassignment(target=field, value=value)
|
|
1086
|
+
else:
|
|
1087
|
+
current = Identifier(name=field, **position)
|
|
1088
|
+
assigned = Reassignment(target=field, value=BinaryOp(
|
|
1089
|
+
left=current, op=COMPOUND_ASSIGNMENT_OPERATORS[operator_token.type], right=value, compound=True,
|
|
1090
|
+
**position))
|
|
1091
|
+
statements = [Assignment(target=receiver, value=target.object), assigned]
|
|
1092
|
+
for statement in statements:
|
|
1093
|
+
statement.line, statement.column = position.get('line', 0), position.get('column', 0)
|
|
1094
|
+
return statements
|
|
1095
|
+
|
|
1096
|
+
def _fresh_receiver_name(self) -> str:
|
|
1097
|
+
"""`assigned_field_receiver<N>`, the lowest N no identifier of the script holds."""
|
|
1098
|
+
if self._token_names is None:
|
|
1099
|
+
self._token_names = {part for token in self.tokens if isinstance(token.value, str)
|
|
1100
|
+
for part in token.value.split('.')}
|
|
1101
|
+
return fresh_name(RECEIVER_NAME_STEM, self._token_names)
|
|
1102
|
+
|
|
1103
|
+
@staticmethod
|
|
1104
|
+
def _refuse_unassignable_target(target: Expression, operator_token: Token, tuple_allowed: bool = False) -> None:
|
|
1105
|
+
"""`arr.get(0).v += 1` is valid Pine, but the tree keeps an assignment only to a name (a
|
|
1106
|
+
dotted field path included) or, for `=`, a tuple. Any other target used to be dropped
|
|
1107
|
+
without a word, leaving a bare expression statement, so it is refused at the operator
|
|
1108
|
+
(card #94). A tuple that is not allowed (`[a, b] := f()`, `[a, b] += ...`) has no receiver
|
|
1109
|
+
and no field, so it gets its own text (card #99)."""
|
|
1110
|
+
if isinstance(target, Identifier) or (tuple_allowed and isinstance(target, ArrayLiteral)):
|
|
1111
|
+
return
|
|
1112
|
+
if isinstance(target, ArrayLiteral):
|
|
1113
|
+
advice = ("to a tuple is not supported; destructure into new names with '=' "
|
|
1114
|
+
"and assign each name with its own ':=' statement")
|
|
1115
|
+
else:
|
|
1116
|
+
advice = 'to the result of an expression; assign the receiver to a variable first and assign its field'
|
|
1117
|
+
raise UnsupportedFeatureError(f'assignment with {operator_token.value!r} {advice}',
|
|
1118
|
+
operator_token.line, operator_token.column)
|
|
1119
|
+
|
|
1120
|
+
def parse_block(self) -> List[Statement]:
|
|
1121
|
+
"""Parse a block of statements (indented)."""
|
|
1122
|
+
statements = []
|
|
1123
|
+
|
|
1124
|
+
while not self.match(TokenType.DEDENT, TokenType.EOF):
|
|
1125
|
+
self.skip_newlines()
|
|
1126
|
+
if self.match(TokenType.DEDENT, TokenType.EOF):
|
|
1127
|
+
break
|
|
1128
|
+
|
|
1129
|
+
statements.extend(_as_statement_list(self._parse_positioned_statement()))
|
|
1130
|
+
|
|
1131
|
+
# Handle comma-separated statements on the same line
|
|
1132
|
+
# e.g., colNum = 8, rowNum = 8
|
|
1133
|
+
while self.match(TokenType.COMMA):
|
|
1134
|
+
self.advance() # consume comma
|
|
1135
|
+
statements.extend(_as_statement_list(self._parse_positioned_statement()))
|
|
1136
|
+
|
|
1137
|
+
return statements
|
|
1138
|
+
|
|
1139
|
+
# ------------------------------------------------------------------ positions (card #28)
|
|
1140
|
+
|
|
1141
|
+
@staticmethod
|
|
1142
|
+
def _position_of(token: Token) -> dict:
|
|
1143
|
+
"""The Pine line and 1-based column of `token`, as node fields: a nested
|
|
1144
|
+
statement, an identifier, a call and a `[]` carry the position a
|
|
1145
|
+
refusal points at. The one source of the positions card #28 adds."""
|
|
1146
|
+
return {'line': token.line, 'column': token.column}
|
|
1147
|
+
|
|
1148
|
+
def _stamp_top_level(self, node, start_token: Token) -> None:
|
|
1149
|
+
"""A top-level item's line, as always, and its column. Only the column
|
|
1150
|
+
goes through _position_of, so switching positions off keeps the line."""
|
|
1151
|
+
node.line = start_token.line
|
|
1152
|
+
node.column = self._position_of(start_token).get('column', 0)
|
|
1153
|
+
|
|
1154
|
+
def _stamp_nested(self, node, start_token: Token):
|
|
1155
|
+
"""A freshly parsed nested statement's position, unless it has one."""
|
|
1156
|
+
if node is not None and not node.line:
|
|
1157
|
+
position = self._position_of(start_token)
|
|
1158
|
+
node.line, node.column = position.get('line', 0), position.get('column', 0)
|
|
1159
|
+
return node
|
|
1160
|
+
|
|
1161
|
+
def _parse_positioned_statement(self):
|
|
1162
|
+
start_token = self.current_token()
|
|
1163
|
+
statement = self.parse_statement()
|
|
1164
|
+
if isinstance(statement, list): # one source statement lowered to several (card #98)
|
|
1165
|
+
return [self._stamp_nested(item, start_token) for item in statement]
|
|
1166
|
+
return self._stamp_nested(statement, start_token)
|
|
1167
|
+
|
|
1168
|
+
def _parse_positioned_single_line_item(self):
|
|
1169
|
+
start_token = self.current_token()
|
|
1170
|
+
item = self._parse_single_line_item()
|
|
1171
|
+
if isinstance(item, (Assignment, VarDecl, VaripDecl)):
|
|
1172
|
+
return self._stamp_nested(item, start_token)
|
|
1173
|
+
return item
|
|
1174
|
+
|
|
1175
|
+
def parse_if_statement(self) -> IfStatement:
|
|
1176
|
+
"""Parse if statement."""
|
|
1177
|
+
self.expect(TokenType.IF)
|
|
1178
|
+
condition = self.parse_expression()
|
|
1179
|
+
|
|
1180
|
+
self.skip_newlines()
|
|
1181
|
+
self.expect(TokenType.INDENT)
|
|
1182
|
+
body = self.parse_block()
|
|
1183
|
+
self.expect(TokenType.DEDENT)
|
|
1184
|
+
|
|
1185
|
+
# Parse else if clauses
|
|
1186
|
+
elseifs = []
|
|
1187
|
+
while self.match(TokenType.ELSE):
|
|
1188
|
+
self.advance()
|
|
1189
|
+
if self.match(TokenType.IF):
|
|
1190
|
+
self.advance()
|
|
1191
|
+
elif_condition = self.parse_expression()
|
|
1192
|
+
self.skip_newlines()
|
|
1193
|
+
self.expect(TokenType.INDENT)
|
|
1194
|
+
elif_body = self.parse_block()
|
|
1195
|
+
self.expect(TokenType.DEDENT)
|
|
1196
|
+
elseifs.append((elif_condition, elif_body))
|
|
1197
|
+
else:
|
|
1198
|
+
# Else clause
|
|
1199
|
+
self.skip_newlines()
|
|
1200
|
+
self.expect(TokenType.INDENT)
|
|
1201
|
+
else_body = self.parse_block()
|
|
1202
|
+
self.expect(TokenType.DEDENT)
|
|
1203
|
+
return IfStatement(condition=condition, body=body, elseifs=elseifs, else_body=else_body)
|
|
1204
|
+
|
|
1205
|
+
return IfStatement(condition=condition, body=body, elseifs=elseifs)
|
|
1206
|
+
|
|
1207
|
+
_ASSIGNMENT_OPERATORS = frozenset({
|
|
1208
|
+
TokenType.ASSIGN, TokenType.REASSIGN, TokenType.PLUS_ASSIGN, TokenType.MINUS_ASSIGN,
|
|
1209
|
+
TokenType.MULT_ASSIGN, TokenType.DIV_ASSIGN, TokenType.MOD_ASSIGN,
|
|
1210
|
+
})
|
|
1211
|
+
# Tokens after `once` that make it a name: `once = 1`, `once := x`, `once.f()`, `once[1]`
|
|
1212
|
+
_ONCE_AS_NAME_FOLLOWERS = _ASSIGNMENT_OPERATORS | {
|
|
1213
|
+
TokenType.DOT, TokenType.LBRACKET, TokenType.COMMA, TokenType.ARROW, TokenType.EOF,
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
def _at_once_structure(self) -> bool:
|
|
1217
|
+
"""At a Pine v6 `once` structure: the identifier `once` whose logical
|
|
1218
|
+
line is followed by an indented block. `once` is not a keyword here, so
|
|
1219
|
+
a variable or function named `once` (Pine v5) stays a name (card #59)."""
|
|
1220
|
+
token = self.current_token()
|
|
1221
|
+
if token.type != TokenType.IDENTIFIER or token.value != 'once':
|
|
1222
|
+
return False
|
|
1223
|
+
if self.peek_token().type in self._ONCE_AS_NAME_FOLLOWERS or self.is_function_declaration():
|
|
1224
|
+
return False
|
|
1225
|
+
index, depth = self.pos + 1, 0
|
|
1226
|
+
while index < len(self.tokens):
|
|
1227
|
+
kind = self.tokens[index].type
|
|
1228
|
+
if kind in (TokenType.LPAREN, TokenType.LBRACKET):
|
|
1229
|
+
depth += 1
|
|
1230
|
+
elif kind in (TokenType.RPAREN, TokenType.RBRACKET):
|
|
1231
|
+
depth -= 1
|
|
1232
|
+
elif kind == TokenType.EOF or (kind == TokenType.NEWLINE and depth <= 0):
|
|
1233
|
+
break
|
|
1234
|
+
index += 1
|
|
1235
|
+
while index < len(self.tokens) and self.tokens[index].type == TokenType.NEWLINE:
|
|
1236
|
+
index += 1
|
|
1237
|
+
return index < len(self.tokens) and self.tokens[index].type == TokenType.INDENT
|
|
1238
|
+
|
|
1239
|
+
def parse_once_statement(self) -> OnceStatement:
|
|
1240
|
+
"""`once [condition]` + an indented block. Pine gives it no `else`."""
|
|
1241
|
+
self.advance() # once
|
|
1242
|
+
condition = None if self.match(TokenType.NEWLINE) else self.parse_expression()
|
|
1243
|
+
self.skip_newlines()
|
|
1244
|
+
self.expect(TokenType.INDENT)
|
|
1245
|
+
body = self.parse_block()
|
|
1246
|
+
self.expect(TokenType.DEDENT)
|
|
1247
|
+
if self.match(TokenType.ELSE):
|
|
1248
|
+
raise self.error("a 'once' structure has no 'else'")
|
|
1249
|
+
return OnceStatement(condition=condition, body=body)
|
|
1250
|
+
|
|
1251
|
+
def parse_for_loop(self) -> Union[ForLoop, ForInLoop]:
|
|
1252
|
+
"""Parse for loop (range or for-in style)."""
|
|
1253
|
+
self.expect(TokenType.FOR)
|
|
1254
|
+
|
|
1255
|
+
# Check for tuple destructuring [a, b] in ...
|
|
1256
|
+
if self.match(TokenType.LBRACKET):
|
|
1257
|
+
return self.parse_for_in_loop()
|
|
1258
|
+
|
|
1259
|
+
# Skip optional type hint: for int i = ... or for float x in ...
|
|
1260
|
+
if self.match(TokenType.TYPE_IDENTIFIER):
|
|
1261
|
+
self.advance() # consume type hint (int, float, etc.)
|
|
1262
|
+
|
|
1263
|
+
# Check for regular 'for var in iterable'
|
|
1264
|
+
var_name = self.expect(TokenType.IDENTIFIER).value
|
|
1265
|
+
|
|
1266
|
+
if self.match(TokenType.IN):
|
|
1267
|
+
# for-in loop
|
|
1268
|
+
self.advance()
|
|
1269
|
+
iterable = self.parse_expression()
|
|
1270
|
+
|
|
1271
|
+
self.skip_newlines()
|
|
1272
|
+
self.expect(TokenType.INDENT)
|
|
1273
|
+
body = self.parse_block()
|
|
1274
|
+
self.expect(TokenType.DEDENT)
|
|
1275
|
+
|
|
1276
|
+
return ForInLoop(vars=[var_name], iterable=iterable, body=body)
|
|
1277
|
+
|
|
1278
|
+
# for var = from to to_val [by step]
|
|
1279
|
+
self.expect(TokenType.ASSIGN)
|
|
1280
|
+
from_val = self.parse_expression()
|
|
1281
|
+
|
|
1282
|
+
self.expect(TokenType.TO)
|
|
1283
|
+
to_val = self.parse_expression()
|
|
1284
|
+
|
|
1285
|
+
step = None
|
|
1286
|
+
if self.match(TokenType.BY):
|
|
1287
|
+
self.advance()
|
|
1288
|
+
step = self.parse_expression()
|
|
1289
|
+
|
|
1290
|
+
self.skip_newlines()
|
|
1291
|
+
self.expect(TokenType.INDENT)
|
|
1292
|
+
body = self.parse_block()
|
|
1293
|
+
self.expect(TokenType.DEDENT)
|
|
1294
|
+
|
|
1295
|
+
return ForLoop(var=var_name, from_val=from_val, to_val=to_val, step=step, body=body)
|
|
1296
|
+
|
|
1297
|
+
def parse_for_in_loop(self) -> ForInLoop:
|
|
1298
|
+
"""Parse for-in loop with tuple destructuring."""
|
|
1299
|
+
self.expect(TokenType.LBRACKET)
|
|
1300
|
+
|
|
1301
|
+
vars = []
|
|
1302
|
+
while not self.match(TokenType.RBRACKET):
|
|
1303
|
+
if self.match(TokenType.COMMA):
|
|
1304
|
+
self.advance()
|
|
1305
|
+
continue
|
|
1306
|
+
|
|
1307
|
+
vars.append(self.expect(TokenType.IDENTIFIER).value)
|
|
1308
|
+
|
|
1309
|
+
self.expect(TokenType.RBRACKET)
|
|
1310
|
+
self.expect(TokenType.IN)
|
|
1311
|
+
|
|
1312
|
+
iterable = self.parse_expression()
|
|
1313
|
+
|
|
1314
|
+
self.skip_newlines()
|
|
1315
|
+
self.expect(TokenType.INDENT)
|
|
1316
|
+
body = self.parse_block()
|
|
1317
|
+
self.expect(TokenType.DEDENT)
|
|
1318
|
+
|
|
1319
|
+
return ForInLoop(vars=vars, iterable=iterable, body=body)
|
|
1320
|
+
|
|
1321
|
+
def parse_while_loop(self) -> WhileLoop:
|
|
1322
|
+
"""Parse while loop."""
|
|
1323
|
+
self.expect(TokenType.WHILE)
|
|
1324
|
+
condition = self.parse_expression()
|
|
1325
|
+
|
|
1326
|
+
self.skip_newlines()
|
|
1327
|
+
self.expect(TokenType.INDENT)
|
|
1328
|
+
body = self.parse_block()
|
|
1329
|
+
self.expect(TokenType.DEDENT)
|
|
1330
|
+
|
|
1331
|
+
return WhileLoop(condition=condition, body=body)
|
|
1332
|
+
|
|
1333
|
+
def _inline_statements(self) -> list:
|
|
1334
|
+
"""The statements of a one-line block, separated by commas (`a := 1, b := 2`)."""
|
|
1335
|
+
statements = _as_statement_list(self._parse_positioned_statement())
|
|
1336
|
+
while self.match(TokenType.COMMA):
|
|
1337
|
+
self.advance()
|
|
1338
|
+
statements.extend(_as_statement_list(self._parse_positioned_statement()))
|
|
1339
|
+
return statements
|
|
1340
|
+
|
|
1341
|
+
def parse_switch_statement(self) -> SwitchStatement:
|
|
1342
|
+
"""Parse switch statement."""
|
|
1343
|
+
self.expect(TokenType.SWITCH)
|
|
1344
|
+
|
|
1345
|
+
# Optional expression
|
|
1346
|
+
expr = None
|
|
1347
|
+
if not self.match(TokenType.NEWLINE):
|
|
1348
|
+
expr = self.parse_expression()
|
|
1349
|
+
|
|
1350
|
+
self.skip_newlines()
|
|
1351
|
+
self.expect(TokenType.INDENT)
|
|
1352
|
+
|
|
1353
|
+
cases = []
|
|
1354
|
+
default = None
|
|
1355
|
+
|
|
1356
|
+
while not self.match(TokenType.DEDENT):
|
|
1357
|
+
self.skip_newlines()
|
|
1358
|
+
if self.match(TokenType.DEDENT):
|
|
1359
|
+
break
|
|
1360
|
+
|
|
1361
|
+
# Check for default case (starts with =>)
|
|
1362
|
+
if self.match(TokenType.ARROW):
|
|
1363
|
+
self.advance() # consume =>
|
|
1364
|
+
self.skip_newlines()
|
|
1365
|
+
if self.match(TokenType.INDENT):
|
|
1366
|
+
self.advance()
|
|
1367
|
+
default = self.parse_block()
|
|
1368
|
+
self.expect(TokenType.DEDENT)
|
|
1369
|
+
else:
|
|
1370
|
+
default = self._inline_statements()
|
|
1371
|
+
self.skip_newlines()
|
|
1372
|
+
continue
|
|
1373
|
+
|
|
1374
|
+
# Parse case expression
|
|
1375
|
+
case_expr = self.parse_expression()
|
|
1376
|
+
|
|
1377
|
+
self.expect(TokenType.ARROW)
|
|
1378
|
+
self.skip_newlines()
|
|
1379
|
+
|
|
1380
|
+
# Parse case body
|
|
1381
|
+
if self.match(TokenType.INDENT):
|
|
1382
|
+
self.advance()
|
|
1383
|
+
case_body = self.parse_block()
|
|
1384
|
+
self.expect(TokenType.DEDENT)
|
|
1385
|
+
else:
|
|
1386
|
+
# Single line, possibly several statements: `'FX' => a := 1, b := 2`
|
|
1387
|
+
case_body = self._inline_statements()
|
|
1388
|
+
|
|
1389
|
+
cases.append((case_expr, case_body))
|
|
1390
|
+
|
|
1391
|
+
self.expect(TokenType.DEDENT)
|
|
1392
|
+
|
|
1393
|
+
return SwitchStatement(expr=expr, cases=cases, default=default)
|
|
1394
|
+
|
|
1395
|
+
# ========================================================================
|
|
1396
|
+
# Expression parsing
|
|
1397
|
+
# ========================================================================
|
|
1398
|
+
|
|
1399
|
+
def parse_expression(self) -> Expression:
|
|
1400
|
+
"""Parse expression (entry point for expression parsing)."""
|
|
1401
|
+
return self.parse_ternary()
|
|
1402
|
+
|
|
1403
|
+
def parse_ternary(self) -> Expression:
|
|
1404
|
+
"""Parse ternary operator: cond ? true_expr : false_expr"""
|
|
1405
|
+
expr = self.parse_or()
|
|
1406
|
+
|
|
1407
|
+
if self.match(TokenType.TERNARY):
|
|
1408
|
+
self.advance()
|
|
1409
|
+
true_expr = self.parse_ternary()
|
|
1410
|
+
self.expect(TokenType.COLON)
|
|
1411
|
+
false_expr = self.parse_ternary()
|
|
1412
|
+
return TernaryOp(condition=expr, true_expr=true_expr, false_expr=false_expr)
|
|
1413
|
+
|
|
1414
|
+
return expr
|
|
1415
|
+
|
|
1416
|
+
def parse_or(self) -> Expression:
|
|
1417
|
+
"""Parse logical OR."""
|
|
1418
|
+
left = self.parse_and()
|
|
1419
|
+
|
|
1420
|
+
while self.match(TokenType.OR):
|
|
1421
|
+
op = self.advance().value
|
|
1422
|
+
right = self.parse_and()
|
|
1423
|
+
left = BinaryOp(left=left, op=op, right=right)
|
|
1424
|
+
|
|
1425
|
+
return left
|
|
1426
|
+
|
|
1427
|
+
def parse_and(self) -> Expression:
|
|
1428
|
+
"""Parse logical AND."""
|
|
1429
|
+
left = self.parse_equality()
|
|
1430
|
+
|
|
1431
|
+
while self.match(TokenType.AND):
|
|
1432
|
+
op = self.advance().value
|
|
1433
|
+
right = self.parse_equality()
|
|
1434
|
+
left = BinaryOp(left=left, op=op, right=right)
|
|
1435
|
+
|
|
1436
|
+
return left
|
|
1437
|
+
|
|
1438
|
+
def parse_equality(self) -> Expression:
|
|
1439
|
+
"""Parse == and != operators."""
|
|
1440
|
+
left = self.parse_comparison()
|
|
1441
|
+
|
|
1442
|
+
while self.match(TokenType.EQ, TokenType.NEQ):
|
|
1443
|
+
op = self.advance().value
|
|
1444
|
+
right = self.parse_comparison()
|
|
1445
|
+
left = BinaryOp(left=left, op=op, right=right)
|
|
1446
|
+
|
|
1447
|
+
return left
|
|
1448
|
+
|
|
1449
|
+
def parse_comparison(self) -> Expression:
|
|
1450
|
+
"""Parse <, >, <=, >= operators."""
|
|
1451
|
+
left = self.parse_addition()
|
|
1452
|
+
|
|
1453
|
+
while self.match(TokenType.LT, TokenType.GT, TokenType.LTE, TokenType.GTE):
|
|
1454
|
+
op = self.advance().value
|
|
1455
|
+
right = self.parse_addition()
|
|
1456
|
+
left = BinaryOp(left=left, op=op, right=right)
|
|
1457
|
+
|
|
1458
|
+
return left
|
|
1459
|
+
|
|
1460
|
+
def parse_addition(self) -> Expression:
|
|
1461
|
+
"""Parse + and - operators."""
|
|
1462
|
+
left = self.parse_multiplication()
|
|
1463
|
+
|
|
1464
|
+
while self.match(TokenType.PLUS, TokenType.MINUS):
|
|
1465
|
+
op = self.advance().value
|
|
1466
|
+
right = self.parse_multiplication()
|
|
1467
|
+
left = BinaryOp(left=left, op=op, right=right)
|
|
1468
|
+
|
|
1469
|
+
return left
|
|
1470
|
+
|
|
1471
|
+
def parse_multiplication(self) -> Expression:
|
|
1472
|
+
"""Parse *, /, % operators."""
|
|
1473
|
+
left = self.parse_unary()
|
|
1474
|
+
|
|
1475
|
+
while self.match(TokenType.MULT, TokenType.DIV, TokenType.MOD):
|
|
1476
|
+
op = self.advance().value
|
|
1477
|
+
right = self.parse_unary()
|
|
1478
|
+
left = BinaryOp(left=left, op=op, right=right)
|
|
1479
|
+
|
|
1480
|
+
return left
|
|
1481
|
+
|
|
1482
|
+
def parse_unary(self) -> Expression:
|
|
1483
|
+
"""Parse unary operators (-, not)."""
|
|
1484
|
+
if self.match(TokenType.MINUS, TokenType.NOT):
|
|
1485
|
+
op = self.advance().value
|
|
1486
|
+
operand = self.parse_unary()
|
|
1487
|
+
return UnaryOp(op=op, operand=operand)
|
|
1488
|
+
|
|
1489
|
+
return self.parse_postfix()
|
|
1490
|
+
|
|
1491
|
+
def parse_postfix(self) -> Expression:
|
|
1492
|
+
"""Parse postfix operations (function calls, member access, indexing)."""
|
|
1493
|
+
expr = self.parse_primary()
|
|
1494
|
+
|
|
1495
|
+
# Block-level expressions (switch/if) already consumed their DEDENT;
|
|
1496
|
+
# don't greedily attach postfix [ or . from the NEXT statement
|
|
1497
|
+
if isinstance(expr, (SwitchExpression, IfExpression)):
|
|
1498
|
+
return expr
|
|
1499
|
+
|
|
1500
|
+
while True:
|
|
1501
|
+
# Function call (may have generic parameters)
|
|
1502
|
+
# Parse as generics only for function/method names, not after index access
|
|
1503
|
+
# This prevents `arr[0] < 5` and `close < ema` from being parsed as generics
|
|
1504
|
+
# Heuristic: generics MUST be followed by ( for function call
|
|
1505
|
+
if (self.match(TokenType.LT) and
|
|
1506
|
+
self.peek_token().type in (TokenType.TYPE_IDENTIFIER, TokenType.IDENTIFIER) and
|
|
1507
|
+
isinstance(expr, (Identifier, MemberAccess)) and
|
|
1508
|
+
self._looks_like_generic_call()): # Additional check
|
|
1509
|
+
# Generic function call: func<T>() or obj.method<T>()
|
|
1510
|
+
generics = self.parse_generic_params()
|
|
1511
|
+
# Add generics to the expression (store as string annotation)
|
|
1512
|
+
if isinstance(expr, Identifier):
|
|
1513
|
+
expr.name = f"{expr.name}<{generics}>"
|
|
1514
|
+
elif isinstance(expr, MemberAccess):
|
|
1515
|
+
expr.member = f"{expr.member}<{generics}>"
|
|
1516
|
+
# Continue to parse the actual function call
|
|
1517
|
+
if self.match(TokenType.LPAREN):
|
|
1518
|
+
expr = self.parse_function_call(expr)
|
|
1519
|
+
elif self.match(TokenType.LPAREN):
|
|
1520
|
+
expr = self.parse_function_call(expr)
|
|
1521
|
+
# Member access
|
|
1522
|
+
elif self.match(TokenType.DOT):
|
|
1523
|
+
self.advance()
|
|
1524
|
+
member = self.expect(TokenType.IDENTIFIER).value
|
|
1525
|
+
|
|
1526
|
+
# Check for generic parameters after member (support both built-in and user-defined types)
|
|
1527
|
+
if (self.match(TokenType.LT) and
|
|
1528
|
+
self.peek_token().type in (TokenType.TYPE_IDENTIFIER, TokenType.IDENTIFIER) and
|
|
1529
|
+
self._looks_like_generic_call()):
|
|
1530
|
+
generics = self.parse_generic_params()
|
|
1531
|
+
member = f"{member}<{generics}>"
|
|
1532
|
+
|
|
1533
|
+
# Check if this is a method call
|
|
1534
|
+
if self.match(TokenType.LPAREN):
|
|
1535
|
+
expr = self.parse_method_call(expr, member)
|
|
1536
|
+
else:
|
|
1537
|
+
expr = MemberAccess(object=expr, member=member)
|
|
1538
|
+
# Index access
|
|
1539
|
+
elif self.match(TokenType.LBRACKET):
|
|
1540
|
+
bracket = self.advance()
|
|
1541
|
+
index = self.parse_expression()
|
|
1542
|
+
self.expect(TokenType.RBRACKET)
|
|
1543
|
+
expr = IndexAccess(object=expr, index=index, **self._position_of(bracket))
|
|
1544
|
+
else:
|
|
1545
|
+
break
|
|
1546
|
+
|
|
1547
|
+
return expr
|
|
1548
|
+
|
|
1549
|
+
def parse_generic_params(self) -> str:
|
|
1550
|
+
"""Parse generic parameters in angle brackets: <T> or <K, V>."""
|
|
1551
|
+
self.expect(TokenType.LT) # consume <
|
|
1552
|
+
|
|
1553
|
+
params = []
|
|
1554
|
+
while not self.match(TokenType.GT):
|
|
1555
|
+
if self.match(TokenType.COMMA):
|
|
1556
|
+
self.advance()
|
|
1557
|
+
continue
|
|
1558
|
+
|
|
1559
|
+
# Parse type parameter
|
|
1560
|
+
if self.match(TokenType.TYPE_IDENTIFIER, TokenType.IDENTIFIER):
|
|
1561
|
+
params.append(self.advance().value)
|
|
1562
|
+
else:
|
|
1563
|
+
raise self.error(f"Expected type parameter, got {self.current_token().type.name}")
|
|
1564
|
+
|
|
1565
|
+
self.expect(TokenType.GT) # consume >
|
|
1566
|
+
|
|
1567
|
+
return ', '.join(params)
|
|
1568
|
+
|
|
1569
|
+
def parse_function_call(self, func: Expression) -> FunctionCall:
|
|
1570
|
+
"""Parse function call arguments."""
|
|
1571
|
+
self.expect(TokenType.LPAREN)
|
|
1572
|
+
|
|
1573
|
+
args = []
|
|
1574
|
+
kwargs = {}
|
|
1575
|
+
|
|
1576
|
+
while not self.match(TokenType.RPAREN):
|
|
1577
|
+
if self.match(TokenType.COMMA):
|
|
1578
|
+
self.advance()
|
|
1579
|
+
continue
|
|
1580
|
+
|
|
1581
|
+
self.skip_newlines()
|
|
1582
|
+
if self.match(TokenType.RPAREN):
|
|
1583
|
+
break
|
|
1584
|
+
|
|
1585
|
+
# Check for keyword argument (can be IDENTIFIER or TYPE_IDENTIFIER like 'color')
|
|
1586
|
+
if self.match(TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER) and self.peek_token().type == TokenType.ASSIGN:
|
|
1587
|
+
key = self.advance().value
|
|
1588
|
+
self.advance() # =
|
|
1589
|
+
value = self.parse_expression()
|
|
1590
|
+
kwargs[key] = value
|
|
1591
|
+
else:
|
|
1592
|
+
# Positional argument
|
|
1593
|
+
args.append(self.parse_expression())
|
|
1594
|
+
|
|
1595
|
+
self.expect(TokenType.RPAREN)
|
|
1596
|
+
|
|
1597
|
+
# Convert func expression to string if it's an identifier or member access
|
|
1598
|
+
if isinstance(func, Identifier):
|
|
1599
|
+
func_name = func.name
|
|
1600
|
+
elif isinstance(func, MemberAccess):
|
|
1601
|
+
func_name = func
|
|
1602
|
+
else:
|
|
1603
|
+
func_name = func
|
|
1604
|
+
|
|
1605
|
+
# Pine v2-v4 `input(defval, title, type=input.float)` is the v5 `input.float(defval, title)` (card #115)
|
|
1606
|
+
if func_name == 'input' and isinstance(kwargs.get('type'), Identifier):
|
|
1607
|
+
from .pine_builtins import LEGACY_INPUT_TYPE_ARGUMENT_FUNCTIONS
|
|
1608
|
+
typed_function = LEGACY_INPUT_TYPE_ARGUMENT_FUNCTIONS.get(kwargs['type'].name.removeprefix('input.'))
|
|
1609
|
+
if typed_function is not None:
|
|
1610
|
+
func_name = typed_function
|
|
1611
|
+
kwargs = {key: value for key, value in kwargs.items() if key != 'type'}
|
|
1612
|
+
|
|
1613
|
+
# a call is placed at its callee's name, where a refusal points (card #28)
|
|
1614
|
+
return FunctionCall(func=func_name, args=args, kwargs=kwargs,
|
|
1615
|
+
line=getattr(func, 'line', 0), column=getattr(func, 'column', 0))
|
|
1616
|
+
|
|
1617
|
+
def parse_method_call(self, obj: Expression, method: str) -> MethodCall:
|
|
1618
|
+
"""Parse method call arguments."""
|
|
1619
|
+
self.expect(TokenType.LPAREN)
|
|
1620
|
+
|
|
1621
|
+
args = []
|
|
1622
|
+
kwargs = {}
|
|
1623
|
+
|
|
1624
|
+
while not self.match(TokenType.RPAREN):
|
|
1625
|
+
if self.match(TokenType.COMMA):
|
|
1626
|
+
self.advance()
|
|
1627
|
+
continue
|
|
1628
|
+
|
|
1629
|
+
# Check for keyword argument (can be IDENTIFIER or TYPE_IDENTIFIER like 'color')
|
|
1630
|
+
if self.match(TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER) and self.peek_token().type == TokenType.ASSIGN:
|
|
1631
|
+
key = self.advance().value
|
|
1632
|
+
self.advance() # =
|
|
1633
|
+
value = self.parse_expression()
|
|
1634
|
+
kwargs[key] = value
|
|
1635
|
+
else:
|
|
1636
|
+
# Positional argument
|
|
1637
|
+
args.append(self.parse_expression())
|
|
1638
|
+
|
|
1639
|
+
self.expect(TokenType.RPAREN)
|
|
1640
|
+
|
|
1641
|
+
return MethodCall(object=obj, method=method, args=args, kwargs=kwargs)
|
|
1642
|
+
|
|
1643
|
+
def parse_primary(self) -> Expression:
|
|
1644
|
+
"""Parse primary expressions (literals, identifiers, parenthesized expressions)."""
|
|
1645
|
+
if self.pos > 0 and self.tokens[self.pos - 1].type in self._ASSIGNMENT_OPERATORS and self._at_once_structure():
|
|
1646
|
+
raise self.error("a 'once' structure returns no value: it cannot be assigned")
|
|
1647
|
+
# Literals
|
|
1648
|
+
if self.match(TokenType.INT_LITERAL):
|
|
1649
|
+
token = self.advance()
|
|
1650
|
+
return Literal(value=token.value, literal_type='int')
|
|
1651
|
+
|
|
1652
|
+
if self.match(TokenType.FLOAT_LITERAL):
|
|
1653
|
+
token = self.advance()
|
|
1654
|
+
return Literal(value=token.value, literal_type='float')
|
|
1655
|
+
|
|
1656
|
+
if self.match(TokenType.STRING_LITERAL):
|
|
1657
|
+
token = self.advance()
|
|
1658
|
+
return Literal(value=token.value, literal_type='string', is_double_quoted=token.is_double_quoted)
|
|
1659
|
+
|
|
1660
|
+
if self.match(TokenType.TRUE):
|
|
1661
|
+
self.advance()
|
|
1662
|
+
return Literal(value=True, literal_type='bool')
|
|
1663
|
+
|
|
1664
|
+
if self.match(TokenType.FALSE):
|
|
1665
|
+
self.advance()
|
|
1666
|
+
return Literal(value=False, literal_type='bool')
|
|
1667
|
+
|
|
1668
|
+
if self.match(TokenType.COLOR_LITERAL):
|
|
1669
|
+
token = self.advance()
|
|
1670
|
+
return Literal(value=token.value, literal_type='color', is_double_quoted=token.is_double_quoted)
|
|
1671
|
+
|
|
1672
|
+
if self.match(TokenType.NA_LITERAL):
|
|
1673
|
+
self.advance()
|
|
1674
|
+
return NaLiteral()
|
|
1675
|
+
|
|
1676
|
+
# Array literal
|
|
1677
|
+
if self.match(TokenType.LBRACKET):
|
|
1678
|
+
self.advance()
|
|
1679
|
+
elements = []
|
|
1680
|
+
|
|
1681
|
+
while not self.match(TokenType.RBRACKET):
|
|
1682
|
+
if self.match(TokenType.COMMA):
|
|
1683
|
+
self.advance()
|
|
1684
|
+
continue
|
|
1685
|
+
|
|
1686
|
+
elements.append(self.parse_expression())
|
|
1687
|
+
|
|
1688
|
+
self.expect(TokenType.RBRACKET)
|
|
1689
|
+
return ArrayLiteral(elements=elements)
|
|
1690
|
+
|
|
1691
|
+
# Parenthesized expression
|
|
1692
|
+
if self.match(TokenType.LPAREN):
|
|
1693
|
+
self.advance()
|
|
1694
|
+
expr = self.parse_expression()
|
|
1695
|
+
self.expect(TokenType.RPAREN)
|
|
1696
|
+
return expr
|
|
1697
|
+
|
|
1698
|
+
# Identifier (`function` too, when a user function is named so: `function(x)`)
|
|
1699
|
+
if self.match(TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER) or (
|
|
1700
|
+
self.match(TokenType.FUNCTION) and self.peek_token().type == TokenType.LPAREN):
|
|
1701
|
+
token = self.advance()
|
|
1702
|
+
return Identifier(name=token.value, **self._position_of(token))
|
|
1703
|
+
|
|
1704
|
+
# If expression
|
|
1705
|
+
if self.match(TokenType.IF):
|
|
1706
|
+
return self.parse_if_expression()
|
|
1707
|
+
|
|
1708
|
+
# Switch expression
|
|
1709
|
+
if self.match(TokenType.SWITCH):
|
|
1710
|
+
return self.parse_switch_expression()
|
|
1711
|
+
|
|
1712
|
+
# For/for-in as value expression: x = for i in arr ...
|
|
1713
|
+
if self.match(TokenType.FOR):
|
|
1714
|
+
return self.parse_for_loop()
|
|
1715
|
+
|
|
1716
|
+
# While as a value expression: x = while cond ... (card #50)
|
|
1717
|
+
if self.match(TokenType.WHILE):
|
|
1718
|
+
return self.parse_while_loop()
|
|
1719
|
+
|
|
1720
|
+
raise self.error(f"Unexpected token in expression: {self.current_token().type.name}")
|
|
1721
|
+
|
|
1722
|
+
def parse_if_expression(self) -> IfExpression:
|
|
1723
|
+
"""Parse if expression (returns a value)."""
|
|
1724
|
+
self.expect(TokenType.IF)
|
|
1725
|
+
condition = self.parse_expression()
|
|
1726
|
+
|
|
1727
|
+
self.skip_newlines()
|
|
1728
|
+
self.expect(TokenType.INDENT)
|
|
1729
|
+
|
|
1730
|
+
# Parse true branch (can be expression or statements)
|
|
1731
|
+
true_expr = self.parse_expression_or_block()
|
|
1732
|
+
|
|
1733
|
+
self.skip_newlines()
|
|
1734
|
+
self.expect(TokenType.DEDENT)
|
|
1735
|
+
|
|
1736
|
+
# Parse else/else-if branch
|
|
1737
|
+
false_expr = None
|
|
1738
|
+
if self.match(TokenType.ELSE):
|
|
1739
|
+
self.advance()
|
|
1740
|
+
self.skip_newlines()
|
|
1741
|
+
if self.match(TokenType.IF):
|
|
1742
|
+
# else if → recurse into another if-expression
|
|
1743
|
+
false_expr = dataclasses.replace(self.parse_if_expression(), chained=True)
|
|
1744
|
+
else:
|
|
1745
|
+
self.expect(TokenType.INDENT)
|
|
1746
|
+
false_expr = self.parse_expression_or_block()
|
|
1747
|
+
self.skip_newlines()
|
|
1748
|
+
self.expect(TokenType.DEDENT)
|
|
1749
|
+
|
|
1750
|
+
return IfExpression(condition=condition, true_expr=true_expr, false_expr=false_expr)
|
|
1751
|
+
|
|
1752
|
+
def parse_switch_expression(self) -> SwitchExpression:
|
|
1753
|
+
"""Parse switch expression (returns a value)."""
|
|
1754
|
+
self.expect(TokenType.SWITCH)
|
|
1755
|
+
|
|
1756
|
+
# Optional expression to switch on
|
|
1757
|
+
expr = None
|
|
1758
|
+
if not self.match(TokenType.NEWLINE):
|
|
1759
|
+
expr = self.parse_expression()
|
|
1760
|
+
|
|
1761
|
+
self.skip_newlines()
|
|
1762
|
+
self.expect(TokenType.INDENT)
|
|
1763
|
+
|
|
1764
|
+
cases = []
|
|
1765
|
+
default = None
|
|
1766
|
+
|
|
1767
|
+
while not self.match(TokenType.DEDENT):
|
|
1768
|
+
self.skip_newlines()
|
|
1769
|
+
if self.match(TokenType.DEDENT):
|
|
1770
|
+
break
|
|
1771
|
+
|
|
1772
|
+
# Check for default case (starts with =>)
|
|
1773
|
+
if self.match(TokenType.ARROW):
|
|
1774
|
+
self.advance() # consume =>
|
|
1775
|
+
self.skip_newlines()
|
|
1776
|
+
# Parse default value — may be a multi-statement block
|
|
1777
|
+
if self.match(TokenType.INDENT):
|
|
1778
|
+
self.advance() # consume INDENT
|
|
1779
|
+
default = self.parse_expression_or_block()
|
|
1780
|
+
self.skip_newlines()
|
|
1781
|
+
self.expect(TokenType.DEDENT)
|
|
1782
|
+
else:
|
|
1783
|
+
default = self._comma_separated_arm(self.parse_expression())
|
|
1784
|
+
self.skip_newlines()
|
|
1785
|
+
continue
|
|
1786
|
+
|
|
1787
|
+
# Parse case condition
|
|
1788
|
+
case_condition = self.parse_expression()
|
|
1789
|
+
|
|
1790
|
+
self.expect(TokenType.ARROW)
|
|
1791
|
+
self.skip_newlines()
|
|
1792
|
+
|
|
1793
|
+
# Parse case value — may be a multi-statement block
|
|
1794
|
+
if self.match(TokenType.INDENT):
|
|
1795
|
+
self.advance() # consume INDENT
|
|
1796
|
+
case_value = self.parse_expression_or_block()
|
|
1797
|
+
self.skip_newlines()
|
|
1798
|
+
self.expect(TokenType.DEDENT)
|
|
1799
|
+
else:
|
|
1800
|
+
case_value = self._comma_separated_arm(self.parse_expression())
|
|
1801
|
+
|
|
1802
|
+
cases.append((case_condition, case_value))
|
|
1803
|
+
self.skip_newlines()
|
|
1804
|
+
|
|
1805
|
+
self.expect(TokenType.DEDENT)
|
|
1806
|
+
|
|
1807
|
+
return SwitchExpression(expr=expr, cases=cases, default=default)
|
|
1808
|
+
|
|
1809
|
+
def _comma_separated_arm(self, first):
|
|
1810
|
+
"""`=> runtime.error("..."), na`: an arm of several expressions is a block whose last one is its
|
|
1811
|
+
value (PyneComp: the call, then the block result)."""
|
|
1812
|
+
if not self.match(TokenType.COMMA):
|
|
1813
|
+
return first
|
|
1814
|
+
statements = [ExpressionStatement(expr=first, line=getattr(first, 'line', 0))]
|
|
1815
|
+
while self.match(TokenType.COMMA):
|
|
1816
|
+
self.advance()
|
|
1817
|
+
expression = self.parse_expression()
|
|
1818
|
+
statements.append(ExpressionStatement(expr=expression, line=getattr(expression, 'line', 0)))
|
|
1819
|
+
return statements
|
|
1820
|
+
|
|
1821
|
+
def parse_expression_or_block(self) -> Union[Expression, List[Statement]]:
|
|
1822
|
+
"""Parse either a single expression or a block of statements.
|
|
1823
|
+
For multi-statement blocks, returns a list of statements where the
|
|
1824
|
+
last expression is the return value.
|
|
1825
|
+
"""
|
|
1826
|
+
# Try to parse as expression first
|
|
1827
|
+
start_pos = self.pos
|
|
1828
|
+
try:
|
|
1829
|
+
expr = self.parse_expression()
|
|
1830
|
+
# A loop parsed as an expression is a statement: a branch is either
|
|
1831
|
+
# a plain expression or a list of statements, never a bare loop (card #50)
|
|
1832
|
+
if isinstance(expr, (ForLoop, ForInLoop, WhileLoop)):
|
|
1833
|
+
expr = [expr]
|
|
1834
|
+
# If next is DEDENT, it's a single expression (end of block)
|
|
1835
|
+
if self.match(TokenType.DEDENT):
|
|
1836
|
+
return expr
|
|
1837
|
+
# If next is NEWLINE, check if more statements follow
|
|
1838
|
+
if self.match(TokenType.NEWLINE):
|
|
1839
|
+
self.skip_newlines()
|
|
1840
|
+
# If DEDENT follows the newlines, it's a single expression
|
|
1841
|
+
if self.match(TokenType.DEDENT):
|
|
1842
|
+
return expr
|
|
1843
|
+
# Otherwise there are more statements — fall through to block parse
|
|
1844
|
+
except:
|
|
1845
|
+
pass
|
|
1846
|
+
|
|
1847
|
+
# Otherwise, parse as block
|
|
1848
|
+
self.pos = start_pos
|
|
1849
|
+
return self.parse_block()
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
def _without_positions(value):
|
|
1853
|
+
"""`value` with every line and column 0, as the parser built nodes before
|
|
1854
|
+
card #28: a non-name element of a destructuring target is emitted as its
|
|
1855
|
+
repr, which must not change."""
|
|
1856
|
+
if isinstance(value, list):
|
|
1857
|
+
return [_without_positions(item) for item in value]
|
|
1858
|
+
if isinstance(value, dict):
|
|
1859
|
+
return {key: _without_positions(item) for key, item in value.items()}
|
|
1860
|
+
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
1861
|
+
return dataclasses.replace(value, line=0, column=0, **{
|
|
1862
|
+
field.name: _without_positions(getattr(value, field.name))
|
|
1863
|
+
for field in dataclasses.fields(value) if field.name not in ('line', 'column')})
|
|
1864
|
+
return value
|
|
1865
|
+
|
|
1866
|
+
|
|
1867
|
+
def parse(tokens: List[Token]) -> Script:
|
|
1868
|
+
"""Convenience function to parse Pine Script tokens into AST.
|
|
1869
|
+
|
|
1870
|
+
A rejected known spelling of valid Pine is repaired, as in transpile() (card #55).
|
|
1871
|
+
"""
|
|
1872
|
+
from .parse_repair import parse_tokens
|
|
1873
|
+
return parse_tokens(tokens)
|