opencode-pine2pyne 0.1.0__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.
- opencode_pine2pyne-0.1.0.dist-info/METADATA +123 -0
- opencode_pine2pyne-0.1.0.dist-info/RECORD +23 -0
- opencode_pine2pyne-0.1.0.dist-info/WHEEL +5 -0
- opencode_pine2pyne-0.1.0.dist-info/entry_points.txt +2 -0
- opencode_pine2pyne-0.1.0.dist-info/licenses/LICENSE +201 -0
- opencode_pine2pyne-0.1.0.dist-info/top_level.txt +1 -0
- pine2pyne/README.md +178 -0
- pine2pyne/TRANSPILER_BEST_PRACTICES.md +425 -0
- pine2pyne/TRANSPILER_USAGE.md +264 -0
- pine2pyne/__init__.py +86 -0
- pine2pyne/__main__.py +12 -0
- pine2pyne/ast_nodes.py +352 -0
- pine2pyne/cli.py +212 -0
- pine2pyne/codegen.py +836 -0
- pine2pyne/errors.py +48 -0
- pine2pyne/import_resolver.py +407 -0
- pine2pyne/lexer.py +546 -0
- pine2pyne/parser.py +1510 -0
- pine2pyne/pine_builtins.py +464 -0
- pine2pyne/symbol_table.py +156 -0
- pine2pyne/tokens.py +143 -0
- pine2pyne/transformer.py +2327 -0
- pine2pyne/type_inference.py +328 -0
pine2pyne/parser.py
ADDED
|
@@ -0,0 +1,1510 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recursive descent parser for Pine Script v6.
|
|
3
|
+
|
|
4
|
+
Converts token stream from lexer into an Abstract Syntax Tree (AST).
|
|
5
|
+
"""
|
|
6
|
+
from typing import List, Optional, Union
|
|
7
|
+
from .tokens import Token, TokenType
|
|
8
|
+
from .ast_nodes import *
|
|
9
|
+
from .errors import ParserError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Parser:
|
|
13
|
+
"""Recursive descent parser for Pine Script v6."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, tokens: List[Token]):
|
|
16
|
+
self.tokens = tokens
|
|
17
|
+
self.pos = 0
|
|
18
|
+
|
|
19
|
+
def current_token(self) -> Token:
|
|
20
|
+
"""Get current token without advancing."""
|
|
21
|
+
if self.pos >= len(self.tokens):
|
|
22
|
+
return self.tokens[-1] # Return EOF
|
|
23
|
+
return self.tokens[self.pos]
|
|
24
|
+
|
|
25
|
+
def peek_token(self, offset: int = 1) -> Token:
|
|
26
|
+
"""Look ahead at token at pos + offset."""
|
|
27
|
+
pos = self.pos + offset
|
|
28
|
+
if pos >= len(self.tokens):
|
|
29
|
+
return self.tokens[-1] # Return EOF
|
|
30
|
+
return self.tokens[pos]
|
|
31
|
+
|
|
32
|
+
def advance(self) -> Token:
|
|
33
|
+
"""Consume and return current token."""
|
|
34
|
+
token = self.current_token()
|
|
35
|
+
if token.type != TokenType.EOF:
|
|
36
|
+
self.pos += 1
|
|
37
|
+
return token
|
|
38
|
+
|
|
39
|
+
def expect(self, token_type: TokenType) -> Token:
|
|
40
|
+
"""Consume token of expected type or raise error."""
|
|
41
|
+
token = self.current_token()
|
|
42
|
+
if token.type != token_type:
|
|
43
|
+
raise ParserError(
|
|
44
|
+
f"Expected {token_type.name}, got {token.type.name}",
|
|
45
|
+
token.line,
|
|
46
|
+
token.column
|
|
47
|
+
)
|
|
48
|
+
return self.advance()
|
|
49
|
+
|
|
50
|
+
def match(self, *token_types: TokenType) -> bool:
|
|
51
|
+
"""Check if current token matches any of the given types."""
|
|
52
|
+
return self.current_token().type in token_types
|
|
53
|
+
|
|
54
|
+
def skip_newlines(self) -> None:
|
|
55
|
+
"""Skip any NEWLINE tokens."""
|
|
56
|
+
while self.match(TokenType.NEWLINE):
|
|
57
|
+
self.advance()
|
|
58
|
+
|
|
59
|
+
def _looks_like_generic_call(self) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Check if the current position looks like a generic function call.
|
|
62
|
+
Pattern: <type1, type2, ...>(args)
|
|
63
|
+
This prevents `close < ema` from being parsed as generics.
|
|
64
|
+
"""
|
|
65
|
+
saved_pos = self.pos
|
|
66
|
+
try:
|
|
67
|
+
# We're currently at <, skip it
|
|
68
|
+
self.pos += 1
|
|
69
|
+
|
|
70
|
+
# Skip through the generic parameters
|
|
71
|
+
depth = 1
|
|
72
|
+
while depth > 0 and self.pos < len(self.tokens):
|
|
73
|
+
token = self.tokens[self.pos]
|
|
74
|
+
if token.type == TokenType.LT:
|
|
75
|
+
depth += 1
|
|
76
|
+
elif token.type == TokenType.GT:
|
|
77
|
+
depth -= 1
|
|
78
|
+
if depth == 0:
|
|
79
|
+
# Found closing >, check next token
|
|
80
|
+
self.pos += 1
|
|
81
|
+
if self.pos < len(self.tokens):
|
|
82
|
+
next_token = self.tokens[self.pos]
|
|
83
|
+
# Generic calls MUST be followed by ( for function call
|
|
84
|
+
self.pos = saved_pos
|
|
85
|
+
return next_token.type == TokenType.LPAREN
|
|
86
|
+
self.pos = saved_pos
|
|
87
|
+
return False
|
|
88
|
+
elif token.type not in (TokenType.TYPE_IDENTIFIER, TokenType.IDENTIFIER,
|
|
89
|
+
TokenType.COMMA, TokenType.LT, TokenType.GT):
|
|
90
|
+
# Only type names and commas are valid inside <...> generics.
|
|
91
|
+
# Any operator (and, or, /, +, etc.) means this is a comparison, not generics.
|
|
92
|
+
self.pos = saved_pos
|
|
93
|
+
return False
|
|
94
|
+
self.pos += 1
|
|
95
|
+
|
|
96
|
+
self.pos = saved_pos
|
|
97
|
+
return False
|
|
98
|
+
except Exception:
|
|
99
|
+
self.pos = saved_pos
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def error(self, message: str) -> ParserError:
|
|
103
|
+
"""Create parser error at current position."""
|
|
104
|
+
token = self.current_token()
|
|
105
|
+
return ParserError(message, token.line, token.column)
|
|
106
|
+
|
|
107
|
+
# ========================================================================
|
|
108
|
+
# Top-level parsing
|
|
109
|
+
# ========================================================================
|
|
110
|
+
|
|
111
|
+
def parse(self) -> Script:
|
|
112
|
+
"""Parse entire Pine Script source."""
|
|
113
|
+
script = Script()
|
|
114
|
+
|
|
115
|
+
self.skip_newlines()
|
|
116
|
+
|
|
117
|
+
# Parse version annotation
|
|
118
|
+
if self.match(TokenType.VERSION_ANNOTATION):
|
|
119
|
+
version_token = self.advance()
|
|
120
|
+
script.version = VersionAnnotation(version=version_token.value)
|
|
121
|
+
self.skip_newlines()
|
|
122
|
+
|
|
123
|
+
# Parse script declaration (indicator/strategy/library)
|
|
124
|
+
if self.match(TokenType.IDENTIFIER):
|
|
125
|
+
if self.current_token().value in ('indicator', 'strategy', 'library'):
|
|
126
|
+
script.script_decl = self.parse_script_declaration()
|
|
127
|
+
self.skip_newlines()
|
|
128
|
+
|
|
129
|
+
# Parse imports
|
|
130
|
+
while self.match(TokenType.IMPORT):
|
|
131
|
+
script.imports.append(self.parse_import())
|
|
132
|
+
self.skip_newlines()
|
|
133
|
+
|
|
134
|
+
# Parse global declarations and body
|
|
135
|
+
# Track actual source line numbers for ordering and blank line detection
|
|
136
|
+
while not self.match(TokenType.EOF):
|
|
137
|
+
self.skip_newlines()
|
|
138
|
+
|
|
139
|
+
if self.match(TokenType.EOF):
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
# Handle VERSION_ANNOTATION tokens that appear in the body
|
|
143
|
+
# (e.g., when pre-version declarations like DEFAULT_PYRAMIDING = 6
|
|
144
|
+
# appear before //@version=6 in the source)
|
|
145
|
+
if self.match(TokenType.VERSION_ANNOTATION):
|
|
146
|
+
if not script.version:
|
|
147
|
+
version_token = self.advance()
|
|
148
|
+
script.version = VersionAnnotation(version=version_token.value)
|
|
149
|
+
else:
|
|
150
|
+
self.advance() # Skip duplicate version annotations
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# Handle COMPILER_ANNOTATION tokens in the body
|
|
154
|
+
if self.match(TokenType.COMPILER_ANNOTATION):
|
|
155
|
+
self.advance() # Skip compiler annotations
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
# Handle strategy/indicator/library declarations that appear after
|
|
159
|
+
# pre-declaration code (when the initial top-level check missed them)
|
|
160
|
+
if (not script.script_decl and
|
|
161
|
+
self.match(TokenType.IDENTIFIER) and
|
|
162
|
+
self.current_token().value in ('indicator', 'strategy', 'library')):
|
|
163
|
+
script.script_decl = self.parse_script_declaration()
|
|
164
|
+
self.skip_newlines()
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
# Record the starting line of each declaration/statement
|
|
168
|
+
start_line = self.current_token().line
|
|
169
|
+
|
|
170
|
+
# Check for function declarations
|
|
171
|
+
if self.is_function_declaration():
|
|
172
|
+
node = self.parse_function_declaration()
|
|
173
|
+
node.line = start_line
|
|
174
|
+
script.declarations.append(node)
|
|
175
|
+
# Check for var/varip declarations
|
|
176
|
+
elif self.match(TokenType.VAR, TokenType.VARIP):
|
|
177
|
+
node = self.parse_var_declaration()
|
|
178
|
+
node.line = start_line
|
|
179
|
+
script.declarations.append(node)
|
|
180
|
+
# Check for type declarations
|
|
181
|
+
elif self.match(TokenType.TYPE):
|
|
182
|
+
node = self.parse_type_declaration()
|
|
183
|
+
node.line = start_line
|
|
184
|
+
script.declarations.append(node)
|
|
185
|
+
# Check for enum declarations
|
|
186
|
+
elif self.match(TokenType.ENUM):
|
|
187
|
+
node = self.parse_enum_declaration()
|
|
188
|
+
node.line = start_line
|
|
189
|
+
script.declarations.append(node)
|
|
190
|
+
# Handle import statements that appear in the body (not just at top)
|
|
191
|
+
elif self.match(TokenType.IMPORT):
|
|
192
|
+
import_node = self.parse_import()
|
|
193
|
+
script.imports.append(import_node)
|
|
194
|
+
# Check for input declarations
|
|
195
|
+
elif self.is_input_declaration():
|
|
196
|
+
saved_pos = self.pos
|
|
197
|
+
try:
|
|
198
|
+
node = self.parse_input_declaration()
|
|
199
|
+
node.line = start_line
|
|
200
|
+
script.declarations.append(node)
|
|
201
|
+
except Exception:
|
|
202
|
+
# Not a simple input declaration (e.g., input.string(...) == "On")
|
|
203
|
+
# Fall back to parsing as a regular statement
|
|
204
|
+
self.pos = saved_pos
|
|
205
|
+
stmt = self.parse_statement()
|
|
206
|
+
if stmt:
|
|
207
|
+
stmt.line = start_line
|
|
208
|
+
script.body.append(stmt)
|
|
209
|
+
# Otherwise, parse as statement
|
|
210
|
+
else:
|
|
211
|
+
stmt = self.parse_statement()
|
|
212
|
+
if stmt:
|
|
213
|
+
stmt.line = start_line
|
|
214
|
+
script.body.append(stmt)
|
|
215
|
+
|
|
216
|
+
self.skip_newlines()
|
|
217
|
+
|
|
218
|
+
return script
|
|
219
|
+
|
|
220
|
+
def parse_script_declaration(self) -> Union[IndicatorDecl, StrategyDecl, LibraryDecl]:
|
|
221
|
+
"""Parse indicator/strategy/library declaration."""
|
|
222
|
+
func_name = self.advance().value # indicator, strategy, or library
|
|
223
|
+
|
|
224
|
+
self.expect(TokenType.LPAREN)
|
|
225
|
+
self.skip_newlines() # Handle newlines after opening paren
|
|
226
|
+
|
|
227
|
+
# Parse title - can be positional string or keyword argument
|
|
228
|
+
title = None
|
|
229
|
+
kwargs = {}
|
|
230
|
+
|
|
231
|
+
# Check if first argument is keyword (IDENTIFIER = value) or positional (STRING_LITERAL)
|
|
232
|
+
if self.match(TokenType.STRING_LITERAL):
|
|
233
|
+
# Positional title argument
|
|
234
|
+
title = self.advance().value
|
|
235
|
+
elif self.match(TokenType.IDENTIFIER):
|
|
236
|
+
# Could be keyword argument like title = "..."
|
|
237
|
+
key = self.advance().value
|
|
238
|
+
self.expect(TokenType.ASSIGN)
|
|
239
|
+
value = self.parse_expression()
|
|
240
|
+
if key == 'title':
|
|
241
|
+
# Extract title value
|
|
242
|
+
if isinstance(value, Literal) and value.literal_type == 'string':
|
|
243
|
+
title = value.value
|
|
244
|
+
else:
|
|
245
|
+
title = str(value)
|
|
246
|
+
else:
|
|
247
|
+
kwargs[key] = value
|
|
248
|
+
|
|
249
|
+
# Parse remaining keyword arguments
|
|
250
|
+
while not self.match(TokenType.RPAREN):
|
|
251
|
+
self.skip_newlines() # Handle newlines before parameters
|
|
252
|
+
|
|
253
|
+
if self.match(TokenType.RPAREN): # Check again after skipping newlines
|
|
254
|
+
break
|
|
255
|
+
|
|
256
|
+
if self.match(TokenType.COMMA):
|
|
257
|
+
self.advance()
|
|
258
|
+
self.skip_newlines() # Handle newlines after comma
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
# Parse kwarg: name = value, or skip positional args
|
|
262
|
+
if (self.match(TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER) and
|
|
263
|
+
self.peek_token().type == TokenType.ASSIGN):
|
|
264
|
+
key = self.advance().value
|
|
265
|
+
self.expect(TokenType.ASSIGN)
|
|
266
|
+
value = self.parse_expression()
|
|
267
|
+
if key == 'title' and title is None:
|
|
268
|
+
if isinstance(value, Literal) and value.literal_type == 'string':
|
|
269
|
+
title = value.value
|
|
270
|
+
else:
|
|
271
|
+
title = str(value)
|
|
272
|
+
else:
|
|
273
|
+
kwargs[key] = value
|
|
274
|
+
else:
|
|
275
|
+
# Positional argument (e.g., "", true, 100) — skip it
|
|
276
|
+
self.parse_expression()
|
|
277
|
+
|
|
278
|
+
self.expect(TokenType.RPAREN)
|
|
279
|
+
|
|
280
|
+
# Use default title if none provided
|
|
281
|
+
if title is None:
|
|
282
|
+
title = f"Untitled {func_name}"
|
|
283
|
+
|
|
284
|
+
if func_name == 'indicator':
|
|
285
|
+
return IndicatorDecl(title=title, kwargs=kwargs)
|
|
286
|
+
elif func_name == 'strategy':
|
|
287
|
+
return StrategyDecl(title=title, kwargs=kwargs)
|
|
288
|
+
else:
|
|
289
|
+
return LibraryDecl(title=title, kwargs=kwargs)
|
|
290
|
+
|
|
291
|
+
def parse_import(self) -> ImportDecl:
|
|
292
|
+
"""Parse import statement: import user/library/version as alias"""
|
|
293
|
+
self.expect(TokenType.IMPORT)
|
|
294
|
+
|
|
295
|
+
# Parse user/library/version as separate tokens joined by DIV (/)
|
|
296
|
+
# e.g., doqkhanh/tafirstlib/2 → IDENTIFIER DIV IDENTIFIER DIV INT_LITERAL
|
|
297
|
+
user = self.expect(TokenType.IDENTIFIER).value
|
|
298
|
+
library = ''
|
|
299
|
+
version = None
|
|
300
|
+
|
|
301
|
+
if self.match(TokenType.DIV):
|
|
302
|
+
self.advance() # consume /
|
|
303
|
+
library = self.expect(TokenType.IDENTIFIER).value
|
|
304
|
+
|
|
305
|
+
if self.match(TokenType.DIV):
|
|
306
|
+
self.advance() # consume /
|
|
307
|
+
# Version can be an integer or identifier
|
|
308
|
+
if self.match(TokenType.INT_LITERAL):
|
|
309
|
+
version = self.advance().value
|
|
310
|
+
elif self.match(TokenType.IDENTIFIER):
|
|
311
|
+
version = self.advance().value
|
|
312
|
+
|
|
313
|
+
# Parse optional 'as alias'
|
|
314
|
+
alias = None
|
|
315
|
+
if self.match(TokenType.IDENTIFIER) and self.current_token().value == 'as':
|
|
316
|
+
self.advance()
|
|
317
|
+
alias = self.expect(TokenType.IDENTIFIER).value
|
|
318
|
+
|
|
319
|
+
return ImportDecl(user=user, library=library, version=version, alias=alias)
|
|
320
|
+
|
|
321
|
+
def is_function_declaration(self) -> bool:
|
|
322
|
+
"""Check if current position is a function declaration."""
|
|
323
|
+
# Look for: [export] [method] name(params) =>
|
|
324
|
+
saved_pos = self.pos
|
|
325
|
+
|
|
326
|
+
# Skip export/method keywords
|
|
327
|
+
if self.match(TokenType.EXPORT):
|
|
328
|
+
self.pos += 1
|
|
329
|
+
if self.match(TokenType.METHOD):
|
|
330
|
+
self.pos += 1
|
|
331
|
+
|
|
332
|
+
# Check for identifier followed by ( and eventually =>
|
|
333
|
+
if not (self.match(TokenType.IDENTIFIER) and self.peek_token().type == TokenType.LPAREN):
|
|
334
|
+
self.pos = saved_pos
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
# Scan ahead to find => arrow (function declarations must have this)
|
|
338
|
+
# Skip past the parameter list
|
|
339
|
+
paren_depth = 0
|
|
340
|
+
while self.pos < len(self.tokens):
|
|
341
|
+
token = self.tokens[self.pos]
|
|
342
|
+
if token.type == TokenType.LPAREN:
|
|
343
|
+
paren_depth += 1
|
|
344
|
+
elif token.type == TokenType.RPAREN:
|
|
345
|
+
paren_depth -= 1
|
|
346
|
+
if paren_depth == 0:
|
|
347
|
+
# Found closing paren, check next token for =>
|
|
348
|
+
self.pos += 1
|
|
349
|
+
if self.pos < len(self.tokens) and self.tokens[self.pos].type == TokenType.ARROW:
|
|
350
|
+
self.pos = saved_pos
|
|
351
|
+
return True
|
|
352
|
+
else:
|
|
353
|
+
self.pos = saved_pos
|
|
354
|
+
return False
|
|
355
|
+
self.pos += 1
|
|
356
|
+
|
|
357
|
+
self.pos = saved_pos
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
def parse_function_declaration(self) -> FuncDecl:
|
|
361
|
+
"""Parse function declaration."""
|
|
362
|
+
is_export = False
|
|
363
|
+
is_method = False
|
|
364
|
+
|
|
365
|
+
if self.match(TokenType.EXPORT):
|
|
366
|
+
is_export = True
|
|
367
|
+
self.advance()
|
|
368
|
+
|
|
369
|
+
if self.match(TokenType.METHOD):
|
|
370
|
+
is_method = True
|
|
371
|
+
self.advance()
|
|
372
|
+
|
|
373
|
+
name = self.expect(TokenType.IDENTIFIER).value
|
|
374
|
+
|
|
375
|
+
# Parse parameters
|
|
376
|
+
self.expect(TokenType.LPAREN)
|
|
377
|
+
self.skip_newlines() # Skip newlines after opening paren
|
|
378
|
+
params = []
|
|
379
|
+
|
|
380
|
+
while not self.match(TokenType.RPAREN):
|
|
381
|
+
self.skip_newlines() # Skip newlines before each parameter
|
|
382
|
+
|
|
383
|
+
if self.match(TokenType.RPAREN):
|
|
384
|
+
break
|
|
385
|
+
|
|
386
|
+
if self.match(TokenType.COMMA):
|
|
387
|
+
self.advance()
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
# Parse parameter: [type] name [= default]
|
|
391
|
+
# Type can be: simple (float), generic (map<K,V>), or array (int[]), or custom type (TradeInfo)
|
|
392
|
+
type_hint = None
|
|
393
|
+
if self.match(TokenType.TYPE_IDENTIFIER):
|
|
394
|
+
# Look ahead to disambiguate type hint vs param name:
|
|
395
|
+
# TYPE_IDENTIFIER + IDENTIFIER → type hint (e.g. "float x")
|
|
396
|
+
# TYPE_IDENTIFIER + LT → generic type hint (e.g. "map<string, int> this")
|
|
397
|
+
# TYPE_IDENTIFIER + RPAREN/COMMA/ASSIGN → param name (e.g. "array" as name)
|
|
398
|
+
next_tok = self.peek_token()
|
|
399
|
+
if next_tok.type in (TokenType.IDENTIFIER, TokenType.LT, TokenType.TYPE_IDENTIFIER):
|
|
400
|
+
type_hint = self.parse_generic_type()
|
|
401
|
+
# else: fall through, treat TYPE_IDENTIFIER as param name
|
|
402
|
+
elif self.match(TokenType.IDENTIFIER) and self.peek_token().type == TokenType.IDENTIFIER:
|
|
403
|
+
# For method parameters like "TradeInfo this" or "int val"
|
|
404
|
+
# First IDENTIFIER is the type, second is the parameter name
|
|
405
|
+
type_hint = self.advance().value
|
|
406
|
+
|
|
407
|
+
# Param name can be IDENTIFIER or TYPE_IDENTIFIER used as a name
|
|
408
|
+
if self.match(TokenType.IDENTIFIER):
|
|
409
|
+
param_name = self.advance().value
|
|
410
|
+
elif self.match(TokenType.TYPE_IDENTIFIER):
|
|
411
|
+
param_name = self.advance().value
|
|
412
|
+
else:
|
|
413
|
+
param_name = self.expect(TokenType.IDENTIFIER).value
|
|
414
|
+
|
|
415
|
+
default = None
|
|
416
|
+
if self.match(TokenType.ASSIGN):
|
|
417
|
+
self.advance()
|
|
418
|
+
default = self.parse_expression()
|
|
419
|
+
|
|
420
|
+
params.append(Parameter(name=param_name, type_hint=type_hint, default=default))
|
|
421
|
+
self.skip_newlines() # Skip newlines after parameter
|
|
422
|
+
|
|
423
|
+
self.expect(TokenType.RPAREN)
|
|
424
|
+
|
|
425
|
+
# Parse => and body
|
|
426
|
+
self.expect(TokenType.ARROW)
|
|
427
|
+
self.skip_newlines()
|
|
428
|
+
|
|
429
|
+
# Check if single-line or multi-line body
|
|
430
|
+
if self.match(TokenType.INDENT):
|
|
431
|
+
# Multi-line body
|
|
432
|
+
self.advance()
|
|
433
|
+
body = self.parse_block()
|
|
434
|
+
self.expect(TokenType.DEDENT)
|
|
435
|
+
else:
|
|
436
|
+
# Single-line body — may have comma-separated statements
|
|
437
|
+
# e.g., fn(x) => a = expr1, b = expr2, return_expr
|
|
438
|
+
body = self._parse_single_line_body()
|
|
439
|
+
|
|
440
|
+
return FuncDecl(name=name, params=params, body=body,
|
|
441
|
+
is_method=is_method, is_export=is_export)
|
|
442
|
+
|
|
443
|
+
def _parse_single_line_body(self):
|
|
444
|
+
"""Parse single-line function body with optional comma-separated statements.
|
|
445
|
+
Pine Script allows: fn(x) => stmt1, stmt2, return_expr
|
|
446
|
+
Each comma-separated item can be an assignment or expression.
|
|
447
|
+
Returns a single expression (no commas) or a list of statements (commas present).
|
|
448
|
+
"""
|
|
449
|
+
# Parse first item — could be var declaration, assignment, or expression
|
|
450
|
+
first = self._parse_single_line_item()
|
|
451
|
+
|
|
452
|
+
# Check for comma (multiple statements)
|
|
453
|
+
if not self.match(TokenType.COMMA):
|
|
454
|
+
return first # Single expression, return as-is
|
|
455
|
+
|
|
456
|
+
# Multiple comma-separated statements
|
|
457
|
+
items = [first]
|
|
458
|
+
while self.match(TokenType.COMMA):
|
|
459
|
+
self.advance() # consume comma
|
|
460
|
+
self.skip_newlines()
|
|
461
|
+
items.append(self._parse_single_line_item())
|
|
462
|
+
|
|
463
|
+
return items
|
|
464
|
+
|
|
465
|
+
def _parse_single_line_item(self):
|
|
466
|
+
"""Parse a single item in a comma-separated single-line function body.
|
|
467
|
+
Can be: var type name = expr, name = expr, or just expr.
|
|
468
|
+
"""
|
|
469
|
+
# Handle 'var' declarations: var type name = expr
|
|
470
|
+
if self.match(TokenType.VAR):
|
|
471
|
+
return self.parse_statement()
|
|
472
|
+
|
|
473
|
+
# Try assignment: name = expr (look ahead for IDENTIFIER followed by ASSIGN)
|
|
474
|
+
saved_pos = self.pos
|
|
475
|
+
if self.match(TokenType.TYPE_IDENTIFIER):
|
|
476
|
+
# Could be typed assignment: float x = expr
|
|
477
|
+
type_hint = self.parse_generic_type()
|
|
478
|
+
if self.match(TokenType.IDENTIFIER):
|
|
479
|
+
var_name = self.advance().value
|
|
480
|
+
if self.match(TokenType.ASSIGN):
|
|
481
|
+
self.advance()
|
|
482
|
+
value = self.parse_expression()
|
|
483
|
+
return Assignment(target=var_name, value=value, type_hint=type_hint)
|
|
484
|
+
self.pos = saved_pos
|
|
485
|
+
|
|
486
|
+
if self.match(TokenType.IDENTIFIER):
|
|
487
|
+
maybe_name = self.current_token().value
|
|
488
|
+
next_pos = self.pos + 1
|
|
489
|
+
if next_pos < len(self.tokens) and self.tokens[next_pos].type == TokenType.ASSIGN:
|
|
490
|
+
var_name = self.advance().value
|
|
491
|
+
self.advance() # consume =
|
|
492
|
+
value = self.parse_expression()
|
|
493
|
+
return Assignment(target=var_name, value=value)
|
|
494
|
+
# Not an assignment — fall through to expression parse
|
|
495
|
+
self.pos = saved_pos
|
|
496
|
+
|
|
497
|
+
return self.parse_expression()
|
|
498
|
+
|
|
499
|
+
def parse_generic_type(self) -> str:
|
|
500
|
+
"""
|
|
501
|
+
Parse type syntax:
|
|
502
|
+
- Modifier + type: simple int, series float, const string, input float
|
|
503
|
+
- Generic: type<T> or map<K, V>
|
|
504
|
+
- Array bracket: type[] (e.g., string[], int[])
|
|
505
|
+
"""
|
|
506
|
+
# Pine Script type modifiers (no Python equivalent, consume and drop)
|
|
507
|
+
TYPE_MODIFIERS = {'simple', 'series', 'const', 'input'}
|
|
508
|
+
|
|
509
|
+
base_type = self.advance().value # Get base type (map, array, string, int, etc.)
|
|
510
|
+
|
|
511
|
+
# Handle type modifiers: simple int, series float, etc.
|
|
512
|
+
# The modifier is consumed but dropped — only the actual type is kept
|
|
513
|
+
if base_type in TYPE_MODIFIERS and self.match(TokenType.TYPE_IDENTIFIER):
|
|
514
|
+
base_type = self.advance().value
|
|
515
|
+
|
|
516
|
+
# Check for array bracket syntax: type[]
|
|
517
|
+
if self.match(TokenType.LBRACKET):
|
|
518
|
+
self.advance() # consume [
|
|
519
|
+
self.expect(TokenType.RBRACKET) # consume ]
|
|
520
|
+
# Convert to generic array syntax: string[] -> array<string>
|
|
521
|
+
return f"array<{base_type}>"
|
|
522
|
+
|
|
523
|
+
# Check for generic parameters: type<T>
|
|
524
|
+
if self.match(TokenType.LT):
|
|
525
|
+
self.advance() # consume <
|
|
526
|
+
|
|
527
|
+
generic_params = []
|
|
528
|
+
while not self.match(TokenType.GT):
|
|
529
|
+
if self.match(TokenType.COMMA):
|
|
530
|
+
self.advance()
|
|
531
|
+
continue
|
|
532
|
+
|
|
533
|
+
# Parse type parameter (can be TYPE_IDENTIFIER or IDENTIFIER)
|
|
534
|
+
if self.match(TokenType.TYPE_IDENTIFIER, TokenType.IDENTIFIER):
|
|
535
|
+
generic_params.append(self.advance().value)
|
|
536
|
+
else:
|
|
537
|
+
raise self.error(f"Expected type parameter, got {self.current_token().type.name}")
|
|
538
|
+
|
|
539
|
+
self.expect(TokenType.GT) # consume >
|
|
540
|
+
|
|
541
|
+
# Format as Python generic: map<string, float> -> dict[str, float]
|
|
542
|
+
return f"{base_type}<{', '.join(generic_params)}>"
|
|
543
|
+
|
|
544
|
+
return base_type
|
|
545
|
+
|
|
546
|
+
def parse_var_declaration(self) -> Union[VarDecl, VaripDecl]:
|
|
547
|
+
"""Parse var/varip declaration."""
|
|
548
|
+
is_var = self.match(TokenType.VAR)
|
|
549
|
+
self.advance() # var or varip
|
|
550
|
+
|
|
551
|
+
# Parse optional type hint (with generic support or UDT name)
|
|
552
|
+
type_hint = None
|
|
553
|
+
# A type keyword directly followed by '=' is being used as the variable
|
|
554
|
+
# NAME, not a type hint — Pine lets a variable shadow a type namespace
|
|
555
|
+
# (e.g. `var matrix = matrix.new<float>(...)`, `var array = ...`). A real
|
|
556
|
+
# type hint is instead followed by a name or a generic '<'. Skip the
|
|
557
|
+
# type-hint branch so the keyword is consumed as the name below.
|
|
558
|
+
if self.match(TokenType.TYPE_IDENTIFIER) and self.peek_token().type != TokenType.ASSIGN:
|
|
559
|
+
type_hint = self.parse_generic_type()
|
|
560
|
+
elif self.match(TokenType.IDENTIFIER):
|
|
561
|
+
# Could be a UDT type name like Statistics, TradeInfo
|
|
562
|
+
next_type = self.peek_token().type
|
|
563
|
+
if next_type == TokenType.IDENTIFIER:
|
|
564
|
+
# Simple UDT: TradeInfo myVar = ...
|
|
565
|
+
type_hint = self.advance().value
|
|
566
|
+
elif next_type in (TokenType.LBRACKET, TokenType.LT):
|
|
567
|
+
# UDT array/generic: TestResult[] results = ..., MyType<T> x = ...
|
|
568
|
+
type_hint = self.advance().value # UDT name
|
|
569
|
+
if self.match(TokenType.LBRACKET):
|
|
570
|
+
self.advance() # consume [
|
|
571
|
+
self.expect(TokenType.RBRACKET) # consume ]
|
|
572
|
+
type_hint = f"array<{type_hint}>"
|
|
573
|
+
elif self.match(TokenType.LT):
|
|
574
|
+
# Parse generic params
|
|
575
|
+
self.advance() # consume <
|
|
576
|
+
generic_params = []
|
|
577
|
+
while not self.match(TokenType.GT):
|
|
578
|
+
if self.match(TokenType.COMMA):
|
|
579
|
+
self.advance()
|
|
580
|
+
continue
|
|
581
|
+
if self.match(TokenType.TYPE_IDENTIFIER, TokenType.IDENTIFIER):
|
|
582
|
+
generic_params.append(self.advance().value)
|
|
583
|
+
else:
|
|
584
|
+
raise self.error(f"Expected type parameter, got {self.current_token().type.name}")
|
|
585
|
+
self.expect(TokenType.GT)
|
|
586
|
+
type_hint = f"{type_hint}<{', '.join(generic_params)}>"
|
|
587
|
+
|
|
588
|
+
# Parse variable name. Accept a TYPE_IDENTIFIER here too: when no type
|
|
589
|
+
# hint was taken, a type keyword in this position is the variable name
|
|
590
|
+
# (`var matrix = ...`), which Pine permits.
|
|
591
|
+
if type_hint is None and self.match(TokenType.TYPE_IDENTIFIER):
|
|
592
|
+
name = self.advance().value
|
|
593
|
+
else:
|
|
594
|
+
name = self.expect(TokenType.IDENTIFIER).value
|
|
595
|
+
|
|
596
|
+
# Parse = value
|
|
597
|
+
self.expect(TokenType.ASSIGN)
|
|
598
|
+
value = self.parse_expression()
|
|
599
|
+
|
|
600
|
+
if is_var:
|
|
601
|
+
return VarDecl(name=name, value=value, type_hint=type_hint)
|
|
602
|
+
else:
|
|
603
|
+
return VaripDecl(name=name, value=value, type_hint=type_hint)
|
|
604
|
+
|
|
605
|
+
def parse_type_declaration(self) -> TypeDecl:
|
|
606
|
+
"""Parse type (UDT) declaration."""
|
|
607
|
+
self.expect(TokenType.TYPE)
|
|
608
|
+
name = self.expect(TokenType.IDENTIFIER).value
|
|
609
|
+
|
|
610
|
+
self.skip_newlines()
|
|
611
|
+
self.expect(TokenType.INDENT)
|
|
612
|
+
|
|
613
|
+
fields = []
|
|
614
|
+
while not self.match(TokenType.DEDENT):
|
|
615
|
+
self.skip_newlines()
|
|
616
|
+
if self.match(TokenType.DEDENT):
|
|
617
|
+
break
|
|
618
|
+
|
|
619
|
+
# Parse field: [var|varip] type name [= default_value]
|
|
620
|
+
# Type can be a built-in type (TYPE_IDENTIFIER), generic (array<T>), or UDT (IDENTIFIER)
|
|
621
|
+
# varip behaves like var since we don't use realtime candles
|
|
622
|
+
field_is_var = False
|
|
623
|
+
if self.match(TokenType.VAR, TokenType.VARIP):
|
|
624
|
+
self.advance() # consume var/varip
|
|
625
|
+
field_is_var = True
|
|
626
|
+
if self.match(TokenType.TYPE_IDENTIFIER):
|
|
627
|
+
type_hint = self.parse_generic_type()
|
|
628
|
+
elif self.match(TokenType.IDENTIFIER):
|
|
629
|
+
type_hint = self.advance().value
|
|
630
|
+
else:
|
|
631
|
+
raise self.error(f"Expected type, got {self.current_token().type.name}")
|
|
632
|
+
field_name = self.expect(TokenType.IDENTIFIER).value
|
|
633
|
+
|
|
634
|
+
# Handle optional default value
|
|
635
|
+
default_value = None
|
|
636
|
+
if self.match(TokenType.ASSIGN):
|
|
637
|
+
self.advance() # consume =
|
|
638
|
+
default_value = self.parse_expression()
|
|
639
|
+
|
|
640
|
+
fields.append((field_name, type_hint, default_value))
|
|
641
|
+
self.skip_newlines()
|
|
642
|
+
|
|
643
|
+
self.expect(TokenType.DEDENT)
|
|
644
|
+
|
|
645
|
+
return TypeDecl(name=name, fields=fields)
|
|
646
|
+
|
|
647
|
+
def parse_enum_declaration(self) -> EnumDecl:
|
|
648
|
+
"""Parse enum declaration."""
|
|
649
|
+
self.expect(TokenType.ENUM)
|
|
650
|
+
# Enum name can be IDENTIFIER or TYPE_IDENTIFIER (e.g., "enum polyline", "enum ta")
|
|
651
|
+
if self.match(TokenType.IDENTIFIER):
|
|
652
|
+
name = self.advance().value
|
|
653
|
+
elif self.match(TokenType.TYPE_IDENTIFIER):
|
|
654
|
+
name = self.advance().value
|
|
655
|
+
else:
|
|
656
|
+
name = self.expect(TokenType.IDENTIFIER).value
|
|
657
|
+
|
|
658
|
+
self.skip_newlines()
|
|
659
|
+
self.expect(TokenType.INDENT)
|
|
660
|
+
|
|
661
|
+
members = []
|
|
662
|
+
while not self.match(TokenType.DEDENT):
|
|
663
|
+
self.skip_newlines()
|
|
664
|
+
if self.match(TokenType.DEDENT):
|
|
665
|
+
break
|
|
666
|
+
|
|
667
|
+
# Parse enum member: name [= value]; can also be TYPE_IDENTIFIER
|
|
668
|
+
if self.match(TokenType.IDENTIFIER):
|
|
669
|
+
member = self.advance().value
|
|
670
|
+
elif self.match(TokenType.TYPE_IDENTIFIER):
|
|
671
|
+
member = self.advance().value
|
|
672
|
+
else:
|
|
673
|
+
member = self.expect(TokenType.IDENTIFIER).value
|
|
674
|
+
|
|
675
|
+
# Check for optional value assignment
|
|
676
|
+
if self.match(TokenType.ASSIGN):
|
|
677
|
+
self.advance() # consume =
|
|
678
|
+
# Parse the value (can be string, int, etc.)
|
|
679
|
+
value = self.parse_expression()
|
|
680
|
+
# Store as tuple (name, value) or just name
|
|
681
|
+
members.append((member, value))
|
|
682
|
+
else:
|
|
683
|
+
members.append(member)
|
|
684
|
+
|
|
685
|
+
self.skip_newlines()
|
|
686
|
+
|
|
687
|
+
self.expect(TokenType.DEDENT)
|
|
688
|
+
|
|
689
|
+
return EnumDecl(name=name, members=members)
|
|
690
|
+
|
|
691
|
+
def is_input_declaration(self) -> bool:
|
|
692
|
+
"""Check if current position is an input declaration.
|
|
693
|
+
Matches: name = input.*() or type name = input.*()
|
|
694
|
+
"""
|
|
695
|
+
saved_pos = self.pos
|
|
696
|
+
|
|
697
|
+
# Skip optional type hint (e.g., float, int, string)
|
|
698
|
+
if self.match(TokenType.TYPE_IDENTIFIER):
|
|
699
|
+
self.pos += 1
|
|
700
|
+
|
|
701
|
+
# Must have an identifier (the variable name)
|
|
702
|
+
if not self.match(TokenType.IDENTIFIER):
|
|
703
|
+
self.pos = saved_pos
|
|
704
|
+
return False
|
|
705
|
+
|
|
706
|
+
self.pos += 1 # Skip identifier
|
|
707
|
+
|
|
708
|
+
# Check for = input.
|
|
709
|
+
result = (self.match(TokenType.ASSIGN) and
|
|
710
|
+
self.peek_token().type == TokenType.IDENTIFIER and
|
|
711
|
+
self.peek_token().value.startswith('input.'))
|
|
712
|
+
|
|
713
|
+
self.pos = saved_pos
|
|
714
|
+
return result
|
|
715
|
+
|
|
716
|
+
def parse_input_declaration(self) -> InputDecl:
|
|
717
|
+
"""Parse input declaration. Handles: name = input.*() and type name = input.*()"""
|
|
718
|
+
# Skip optional type hint
|
|
719
|
+
if self.match(TokenType.TYPE_IDENTIFIER):
|
|
720
|
+
self.advance()
|
|
721
|
+
|
|
722
|
+
name = self.advance().value
|
|
723
|
+
self.expect(TokenType.ASSIGN)
|
|
724
|
+
|
|
725
|
+
# Parse input.*() call
|
|
726
|
+
func_call = self.parse_expression()
|
|
727
|
+
|
|
728
|
+
if not isinstance(func_call, FunctionCall):
|
|
729
|
+
raise self.error("Expected input function call")
|
|
730
|
+
|
|
731
|
+
# Extract function name and arguments
|
|
732
|
+
if isinstance(func_call.func, str):
|
|
733
|
+
func = func_call.func
|
|
734
|
+
elif isinstance(func_call.func, MemberAccess):
|
|
735
|
+
func = f"{func_call.func.object}.{func_call.func.member}"
|
|
736
|
+
else:
|
|
737
|
+
func = "input"
|
|
738
|
+
|
|
739
|
+
return InputDecl(name=name, func=func, args=func_call.args, kwargs=func_call.kwargs)
|
|
740
|
+
|
|
741
|
+
# ========================================================================
|
|
742
|
+
# Statement parsing
|
|
743
|
+
# ========================================================================
|
|
744
|
+
|
|
745
|
+
def parse_statement(self) -> Optional[Statement]:
|
|
746
|
+
"""Parse a single statement."""
|
|
747
|
+
self.skip_newlines()
|
|
748
|
+
|
|
749
|
+
# If statement
|
|
750
|
+
if self.match(TokenType.IF):
|
|
751
|
+
return self.parse_if_statement()
|
|
752
|
+
|
|
753
|
+
# For loop
|
|
754
|
+
if self.match(TokenType.FOR):
|
|
755
|
+
return self.parse_for_loop()
|
|
756
|
+
|
|
757
|
+
# While loop
|
|
758
|
+
if self.match(TokenType.WHILE):
|
|
759
|
+
return self.parse_while_loop()
|
|
760
|
+
|
|
761
|
+
# Switch statement
|
|
762
|
+
if self.match(TokenType.SWITCH):
|
|
763
|
+
return self.parse_switch_statement()
|
|
764
|
+
|
|
765
|
+
# Break
|
|
766
|
+
if self.match(TokenType.BREAK):
|
|
767
|
+
self.advance()
|
|
768
|
+
return BreakStatement()
|
|
769
|
+
|
|
770
|
+
# Continue
|
|
771
|
+
if self.match(TokenType.CONTINUE):
|
|
772
|
+
self.advance()
|
|
773
|
+
return ContinueStatement()
|
|
774
|
+
|
|
775
|
+
# Var/varip declarations (can appear inside blocks in Pine Script v6)
|
|
776
|
+
if self.match(TokenType.VAR, TokenType.VARIP):
|
|
777
|
+
return self.parse_var_declaration()
|
|
778
|
+
|
|
779
|
+
# Local variable declaration with explicit type: type name = value
|
|
780
|
+
# Examples: float x = 10.0, string[] ids = array.new_string()
|
|
781
|
+
# Also handles user-defined types: Signal signalState = ...
|
|
782
|
+
# A leading type keyword is only a declaration when followed by the var
|
|
783
|
+
# NAME (IDENTIFIER), a generic '<', or an array '[]'. Anything else means
|
|
784
|
+
# the keyword is being used as a VALUE — a type-conversion call
|
|
785
|
+
# `bool(na)` (LPAREN), a bare variable reference `matrix` (NEWLINE, when a
|
|
786
|
+
# variable shadows the type namespace), member access `matrix.set(...)`
|
|
787
|
+
# (DOT), or history access `matrix[0]` — all of which are expressions.
|
|
788
|
+
# A following TYPE_IDENTIFIER also means a decl — a qualifier/modifier
|
|
789
|
+
# chain such as `const matrix<T> m`, `series int x`, `simple string s`.
|
|
790
|
+
_pk = self.peek_token().type
|
|
791
|
+
_is_typed_decl = _pk in (TokenType.LT, TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER) or (
|
|
792
|
+
_pk == TokenType.LBRACKET
|
|
793
|
+
and self.pos + 2 < len(self.tokens)
|
|
794
|
+
and self.tokens[self.pos + 2].type == TokenType.RBRACKET
|
|
795
|
+
)
|
|
796
|
+
if self.match(TokenType.TYPE_IDENTIFIER) and _is_typed_decl:
|
|
797
|
+
type_hint = self.parse_generic_type()
|
|
798
|
+
var_name = self.expect(TokenType.IDENTIFIER).value
|
|
799
|
+
self.expect(TokenType.ASSIGN)
|
|
800
|
+
value = self.parse_expression()
|
|
801
|
+
# Return as a local variable assignment with type hint
|
|
802
|
+
return Assignment(target=var_name, value=value, type_hint=type_hint)
|
|
803
|
+
|
|
804
|
+
# Check for user-defined type declaration: UDT varName = value
|
|
805
|
+
# Heuristic: IDENTIFIER IDENTIFIER ASSIGN (e.g., Signal signalState =)
|
|
806
|
+
if (self.match(TokenType.IDENTIFIER) and
|
|
807
|
+
self.pos + 1 < len(self.tokens) and
|
|
808
|
+
self.tokens[self.pos + 1].type == TokenType.IDENTIFIER and
|
|
809
|
+
self.pos + 2 < len(self.tokens) and
|
|
810
|
+
self.tokens[self.pos + 2].type == TokenType.ASSIGN):
|
|
811
|
+
type_name = self.advance().value
|
|
812
|
+
var_name = self.advance().value
|
|
813
|
+
self.expect(TokenType.ASSIGN)
|
|
814
|
+
value = self.parse_expression()
|
|
815
|
+
return Assignment(target=var_name, value=value, type_hint=type_name)
|
|
816
|
+
|
|
817
|
+
# Check for UDT array declaration: UDT[] varName = value
|
|
818
|
+
# Heuristic: IDENTIFIER LBRACKET RBRACKET IDENTIFIER ASSIGN
|
|
819
|
+
if (self.match(TokenType.IDENTIFIER) and
|
|
820
|
+
self.pos + 1 < len(self.tokens) and
|
|
821
|
+
self.tokens[self.pos + 1].type == TokenType.LBRACKET and
|
|
822
|
+
self.pos + 2 < len(self.tokens) and
|
|
823
|
+
self.tokens[self.pos + 2].type == TokenType.RBRACKET and
|
|
824
|
+
self.pos + 3 < len(self.tokens) and
|
|
825
|
+
self.tokens[self.pos + 3].type == TokenType.IDENTIFIER and
|
|
826
|
+
self.pos + 4 < len(self.tokens) and
|
|
827
|
+
self.tokens[self.pos + 4].type == TokenType.ASSIGN):
|
|
828
|
+
udt_name = self.advance().value
|
|
829
|
+
self.advance() # consume [
|
|
830
|
+
self.advance() # consume ]
|
|
831
|
+
var_name = self.advance().value
|
|
832
|
+
self.expect(TokenType.ASSIGN)
|
|
833
|
+
value = self.parse_expression()
|
|
834
|
+
return Assignment(target=var_name, value=value, type_hint=f"array<{udt_name}>")
|
|
835
|
+
|
|
836
|
+
# Check for nested function declarations: name(params) =>
|
|
837
|
+
if self.is_function_declaration():
|
|
838
|
+
return self.parse_function_declaration()
|
|
839
|
+
|
|
840
|
+
# Assignment or expression statement
|
|
841
|
+
expr = self.parse_expression()
|
|
842
|
+
|
|
843
|
+
# Check for assignment/reassignment
|
|
844
|
+
if self.match(TokenType.ASSIGN):
|
|
845
|
+
self.advance()
|
|
846
|
+
value = self.parse_expression()
|
|
847
|
+
|
|
848
|
+
if isinstance(expr, Identifier):
|
|
849
|
+
return Assignment(target=expr.name, value=value)
|
|
850
|
+
elif isinstance(expr, ArrayLiteral):
|
|
851
|
+
# Tuple destructuring
|
|
852
|
+
names = [e.name if isinstance(e, Identifier) else str(e) for e in expr.elements]
|
|
853
|
+
return Assignment(target=TupleDestructure(names=names, value=value), value=value)
|
|
854
|
+
|
|
855
|
+
elif self.match(TokenType.REASSIGN):
|
|
856
|
+
self.advance()
|
|
857
|
+
value = self.parse_expression()
|
|
858
|
+
|
|
859
|
+
if isinstance(expr, Identifier):
|
|
860
|
+
return Reassignment(target=expr.name, value=value)
|
|
861
|
+
|
|
862
|
+
# Compound assignment operators: +=, -=, *=, /=, %=
|
|
863
|
+
elif self.match(TokenType.PLUS_ASSIGN, TokenType.MINUS_ASSIGN, TokenType.MULT_ASSIGN,
|
|
864
|
+
TokenType.DIV_ASSIGN, TokenType.MOD_ASSIGN):
|
|
865
|
+
op_token = self.advance()
|
|
866
|
+
right_value = self.parse_expression()
|
|
867
|
+
|
|
868
|
+
if isinstance(expr, Identifier):
|
|
869
|
+
# Convert x += 5 to x := x + 5
|
|
870
|
+
# Determine the operator symbol
|
|
871
|
+
op_map = {
|
|
872
|
+
TokenType.PLUS_ASSIGN: '+',
|
|
873
|
+
TokenType.MINUS_ASSIGN: '-',
|
|
874
|
+
TokenType.MULT_ASSIGN: '*',
|
|
875
|
+
TokenType.DIV_ASSIGN: '/',
|
|
876
|
+
TokenType.MOD_ASSIGN: '%',
|
|
877
|
+
}
|
|
878
|
+
op_symbol = op_map[op_token.type]
|
|
879
|
+
|
|
880
|
+
# Create binary expression: x + 5
|
|
881
|
+
from .ast_nodes import BinaryOp
|
|
882
|
+
new_value = BinaryOp(left=expr, op=op_symbol, right=right_value)
|
|
883
|
+
|
|
884
|
+
# Return reassignment: x := (x + 5)
|
|
885
|
+
return Reassignment(target=expr.name, value=new_value)
|
|
886
|
+
|
|
887
|
+
# Expression statement
|
|
888
|
+
return ExpressionStatement(expr=expr)
|
|
889
|
+
|
|
890
|
+
def parse_block(self) -> List[Statement]:
|
|
891
|
+
"""Parse a block of statements (indented)."""
|
|
892
|
+
statements = []
|
|
893
|
+
|
|
894
|
+
while not self.match(TokenType.DEDENT, TokenType.EOF):
|
|
895
|
+
self.skip_newlines()
|
|
896
|
+
if self.match(TokenType.DEDENT, TokenType.EOF):
|
|
897
|
+
break
|
|
898
|
+
|
|
899
|
+
stmt = self.parse_statement()
|
|
900
|
+
if stmt:
|
|
901
|
+
statements.append(stmt)
|
|
902
|
+
|
|
903
|
+
# Handle comma-separated statements on the same line
|
|
904
|
+
# e.g., colNum = 8, rowNum = 8
|
|
905
|
+
while self.match(TokenType.COMMA):
|
|
906
|
+
self.advance() # consume comma
|
|
907
|
+
stmt = self.parse_statement()
|
|
908
|
+
if stmt:
|
|
909
|
+
statements.append(stmt)
|
|
910
|
+
|
|
911
|
+
return statements
|
|
912
|
+
|
|
913
|
+
def parse_if_statement(self) -> IfStatement:
|
|
914
|
+
"""Parse if statement."""
|
|
915
|
+
self.expect(TokenType.IF)
|
|
916
|
+
condition = self.parse_expression()
|
|
917
|
+
|
|
918
|
+
self.skip_newlines()
|
|
919
|
+
self.expect(TokenType.INDENT)
|
|
920
|
+
body = self.parse_block()
|
|
921
|
+
self.expect(TokenType.DEDENT)
|
|
922
|
+
|
|
923
|
+
# Parse else if clauses
|
|
924
|
+
elseifs = []
|
|
925
|
+
while self.match(TokenType.ELSE):
|
|
926
|
+
self.advance()
|
|
927
|
+
if self.match(TokenType.IF):
|
|
928
|
+
self.advance()
|
|
929
|
+
elif_condition = self.parse_expression()
|
|
930
|
+
self.skip_newlines()
|
|
931
|
+
self.expect(TokenType.INDENT)
|
|
932
|
+
elif_body = self.parse_block()
|
|
933
|
+
self.expect(TokenType.DEDENT)
|
|
934
|
+
elseifs.append((elif_condition, elif_body))
|
|
935
|
+
else:
|
|
936
|
+
# Else clause
|
|
937
|
+
self.skip_newlines()
|
|
938
|
+
self.expect(TokenType.INDENT)
|
|
939
|
+
else_body = self.parse_block()
|
|
940
|
+
self.expect(TokenType.DEDENT)
|
|
941
|
+
return IfStatement(condition=condition, body=body, elseifs=elseifs, else_body=else_body)
|
|
942
|
+
|
|
943
|
+
return IfStatement(condition=condition, body=body, elseifs=elseifs)
|
|
944
|
+
|
|
945
|
+
def parse_for_loop(self) -> Union[ForLoop, ForInLoop]:
|
|
946
|
+
"""Parse for loop (range or for-in style)."""
|
|
947
|
+
self.expect(TokenType.FOR)
|
|
948
|
+
|
|
949
|
+
# Check for tuple destructuring [a, b] in ...
|
|
950
|
+
if self.match(TokenType.LBRACKET):
|
|
951
|
+
return self.parse_for_in_loop()
|
|
952
|
+
|
|
953
|
+
# Skip optional type hint: for int i = ... or for float x in ...
|
|
954
|
+
if self.match(TokenType.TYPE_IDENTIFIER):
|
|
955
|
+
self.advance() # consume type hint (int, float, etc.)
|
|
956
|
+
|
|
957
|
+
# Check for regular 'for var in iterable'
|
|
958
|
+
var_name = self.expect(TokenType.IDENTIFIER).value
|
|
959
|
+
|
|
960
|
+
if self.match(TokenType.IN):
|
|
961
|
+
# for-in loop
|
|
962
|
+
self.advance()
|
|
963
|
+
iterable = self.parse_expression()
|
|
964
|
+
|
|
965
|
+
self.skip_newlines()
|
|
966
|
+
self.expect(TokenType.INDENT)
|
|
967
|
+
body = self.parse_block()
|
|
968
|
+
self.expect(TokenType.DEDENT)
|
|
969
|
+
|
|
970
|
+
return ForInLoop(vars=[var_name], iterable=iterable, body=body)
|
|
971
|
+
|
|
972
|
+
# for var = from to to_val [by step]
|
|
973
|
+
self.expect(TokenType.ASSIGN)
|
|
974
|
+
from_val = self.parse_expression()
|
|
975
|
+
|
|
976
|
+
self.expect(TokenType.TO)
|
|
977
|
+
to_val = self.parse_expression()
|
|
978
|
+
|
|
979
|
+
step = None
|
|
980
|
+
if self.match(TokenType.BY):
|
|
981
|
+
self.advance()
|
|
982
|
+
step = self.parse_expression()
|
|
983
|
+
|
|
984
|
+
self.skip_newlines()
|
|
985
|
+
self.expect(TokenType.INDENT)
|
|
986
|
+
body = self.parse_block()
|
|
987
|
+
self.expect(TokenType.DEDENT)
|
|
988
|
+
|
|
989
|
+
return ForLoop(var=var_name, from_val=from_val, to_val=to_val, step=step, body=body)
|
|
990
|
+
|
|
991
|
+
def parse_for_in_loop(self) -> ForInLoop:
|
|
992
|
+
"""Parse for-in loop with tuple destructuring."""
|
|
993
|
+
self.expect(TokenType.LBRACKET)
|
|
994
|
+
|
|
995
|
+
vars = []
|
|
996
|
+
while not self.match(TokenType.RBRACKET):
|
|
997
|
+
if self.match(TokenType.COMMA):
|
|
998
|
+
self.advance()
|
|
999
|
+
continue
|
|
1000
|
+
|
|
1001
|
+
vars.append(self.expect(TokenType.IDENTIFIER).value)
|
|
1002
|
+
|
|
1003
|
+
self.expect(TokenType.RBRACKET)
|
|
1004
|
+
self.expect(TokenType.IN)
|
|
1005
|
+
|
|
1006
|
+
iterable = self.parse_expression()
|
|
1007
|
+
|
|
1008
|
+
self.skip_newlines()
|
|
1009
|
+
self.expect(TokenType.INDENT)
|
|
1010
|
+
body = self.parse_block()
|
|
1011
|
+
self.expect(TokenType.DEDENT)
|
|
1012
|
+
|
|
1013
|
+
return ForInLoop(vars=vars, iterable=iterable, body=body)
|
|
1014
|
+
|
|
1015
|
+
def parse_while_loop(self) -> WhileLoop:
|
|
1016
|
+
"""Parse while loop."""
|
|
1017
|
+
self.expect(TokenType.WHILE)
|
|
1018
|
+
condition = self.parse_expression()
|
|
1019
|
+
|
|
1020
|
+
self.skip_newlines()
|
|
1021
|
+
self.expect(TokenType.INDENT)
|
|
1022
|
+
body = self.parse_block()
|
|
1023
|
+
self.expect(TokenType.DEDENT)
|
|
1024
|
+
|
|
1025
|
+
return WhileLoop(condition=condition, body=body)
|
|
1026
|
+
|
|
1027
|
+
def parse_switch_statement(self) -> SwitchStatement:
|
|
1028
|
+
"""Parse switch statement."""
|
|
1029
|
+
self.expect(TokenType.SWITCH)
|
|
1030
|
+
|
|
1031
|
+
# Optional expression
|
|
1032
|
+
expr = None
|
|
1033
|
+
if not self.match(TokenType.NEWLINE):
|
|
1034
|
+
expr = self.parse_expression()
|
|
1035
|
+
|
|
1036
|
+
self.skip_newlines()
|
|
1037
|
+
self.expect(TokenType.INDENT)
|
|
1038
|
+
|
|
1039
|
+
cases = []
|
|
1040
|
+
default = None
|
|
1041
|
+
|
|
1042
|
+
while not self.match(TokenType.DEDENT):
|
|
1043
|
+
self.skip_newlines()
|
|
1044
|
+
if self.match(TokenType.DEDENT):
|
|
1045
|
+
break
|
|
1046
|
+
|
|
1047
|
+
# Check for default case (starts with =>)
|
|
1048
|
+
if self.match(TokenType.ARROW):
|
|
1049
|
+
self.advance() # consume =>
|
|
1050
|
+
self.skip_newlines()
|
|
1051
|
+
if self.match(TokenType.INDENT):
|
|
1052
|
+
self.advance()
|
|
1053
|
+
default = self.parse_block()
|
|
1054
|
+
self.expect(TokenType.DEDENT)
|
|
1055
|
+
else:
|
|
1056
|
+
case_stmt = self.parse_statement()
|
|
1057
|
+
default = [case_stmt] if case_stmt else []
|
|
1058
|
+
self.skip_newlines()
|
|
1059
|
+
continue
|
|
1060
|
+
|
|
1061
|
+
# Parse case expression
|
|
1062
|
+
case_expr = self.parse_expression()
|
|
1063
|
+
|
|
1064
|
+
self.expect(TokenType.ARROW)
|
|
1065
|
+
self.skip_newlines()
|
|
1066
|
+
|
|
1067
|
+
# Parse case body
|
|
1068
|
+
if self.match(TokenType.INDENT):
|
|
1069
|
+
self.advance()
|
|
1070
|
+
case_body = self.parse_block()
|
|
1071
|
+
self.expect(TokenType.DEDENT)
|
|
1072
|
+
else:
|
|
1073
|
+
# Single line
|
|
1074
|
+
case_stmt = self.parse_statement()
|
|
1075
|
+
case_body = [case_stmt] if case_stmt else []
|
|
1076
|
+
|
|
1077
|
+
cases.append((case_expr, case_body))
|
|
1078
|
+
|
|
1079
|
+
self.expect(TokenType.DEDENT)
|
|
1080
|
+
|
|
1081
|
+
return SwitchStatement(expr=expr, cases=cases, default=default)
|
|
1082
|
+
|
|
1083
|
+
# ========================================================================
|
|
1084
|
+
# Expression parsing
|
|
1085
|
+
# ========================================================================
|
|
1086
|
+
|
|
1087
|
+
def parse_expression(self) -> Expression:
|
|
1088
|
+
"""Parse expression (entry point for expression parsing)."""
|
|
1089
|
+
return self.parse_ternary()
|
|
1090
|
+
|
|
1091
|
+
def parse_ternary(self) -> Expression:
|
|
1092
|
+
"""Parse ternary operator: cond ? true_expr : false_expr"""
|
|
1093
|
+
expr = self.parse_or()
|
|
1094
|
+
|
|
1095
|
+
if self.match(TokenType.TERNARY):
|
|
1096
|
+
self.advance()
|
|
1097
|
+
true_expr = self.parse_ternary()
|
|
1098
|
+
self.expect(TokenType.COLON)
|
|
1099
|
+
false_expr = self.parse_ternary()
|
|
1100
|
+
return TernaryOp(condition=expr, true_expr=true_expr, false_expr=false_expr)
|
|
1101
|
+
|
|
1102
|
+
return expr
|
|
1103
|
+
|
|
1104
|
+
def parse_or(self) -> Expression:
|
|
1105
|
+
"""Parse logical OR."""
|
|
1106
|
+
left = self.parse_and()
|
|
1107
|
+
|
|
1108
|
+
while self.match(TokenType.OR):
|
|
1109
|
+
op = self.advance().value
|
|
1110
|
+
right = self.parse_and()
|
|
1111
|
+
left = BinaryOp(left=left, op=op, right=right)
|
|
1112
|
+
|
|
1113
|
+
return left
|
|
1114
|
+
|
|
1115
|
+
def parse_and(self) -> Expression:
|
|
1116
|
+
"""Parse logical AND."""
|
|
1117
|
+
left = self.parse_equality()
|
|
1118
|
+
|
|
1119
|
+
while self.match(TokenType.AND):
|
|
1120
|
+
op = self.advance().value
|
|
1121
|
+
right = self.parse_equality()
|
|
1122
|
+
left = BinaryOp(left=left, op=op, right=right)
|
|
1123
|
+
|
|
1124
|
+
return left
|
|
1125
|
+
|
|
1126
|
+
def parse_equality(self) -> Expression:
|
|
1127
|
+
"""Parse == and != operators."""
|
|
1128
|
+
left = self.parse_comparison()
|
|
1129
|
+
|
|
1130
|
+
while self.match(TokenType.EQ, TokenType.NEQ):
|
|
1131
|
+
op = self.advance().value
|
|
1132
|
+
right = self.parse_comparison()
|
|
1133
|
+
left = BinaryOp(left=left, op=op, right=right)
|
|
1134
|
+
|
|
1135
|
+
return left
|
|
1136
|
+
|
|
1137
|
+
def parse_comparison(self) -> Expression:
|
|
1138
|
+
"""Parse <, >, <=, >= operators."""
|
|
1139
|
+
left = self.parse_addition()
|
|
1140
|
+
|
|
1141
|
+
while self.match(TokenType.LT, TokenType.GT, TokenType.LTE, TokenType.GTE):
|
|
1142
|
+
op = self.advance().value
|
|
1143
|
+
right = self.parse_addition()
|
|
1144
|
+
left = BinaryOp(left=left, op=op, right=right)
|
|
1145
|
+
|
|
1146
|
+
return left
|
|
1147
|
+
|
|
1148
|
+
def parse_addition(self) -> Expression:
|
|
1149
|
+
"""Parse + and - operators."""
|
|
1150
|
+
left = self.parse_multiplication()
|
|
1151
|
+
|
|
1152
|
+
while self.match(TokenType.PLUS, TokenType.MINUS):
|
|
1153
|
+
op = self.advance().value
|
|
1154
|
+
right = self.parse_multiplication()
|
|
1155
|
+
left = BinaryOp(left=left, op=op, right=right)
|
|
1156
|
+
|
|
1157
|
+
return left
|
|
1158
|
+
|
|
1159
|
+
def parse_multiplication(self) -> Expression:
|
|
1160
|
+
"""Parse *, /, % operators."""
|
|
1161
|
+
left = self.parse_unary()
|
|
1162
|
+
|
|
1163
|
+
while self.match(TokenType.MULT, TokenType.DIV, TokenType.MOD):
|
|
1164
|
+
op = self.advance().value
|
|
1165
|
+
right = self.parse_unary()
|
|
1166
|
+
left = BinaryOp(left=left, op=op, right=right)
|
|
1167
|
+
|
|
1168
|
+
return left
|
|
1169
|
+
|
|
1170
|
+
def parse_unary(self) -> Expression:
|
|
1171
|
+
"""Parse unary operators (-, not)."""
|
|
1172
|
+
if self.match(TokenType.MINUS, TokenType.NOT):
|
|
1173
|
+
op = self.advance().value
|
|
1174
|
+
operand = self.parse_unary()
|
|
1175
|
+
return UnaryOp(op=op, operand=operand)
|
|
1176
|
+
|
|
1177
|
+
return self.parse_postfix()
|
|
1178
|
+
|
|
1179
|
+
def parse_postfix(self) -> Expression:
|
|
1180
|
+
"""Parse postfix operations (function calls, member access, indexing)."""
|
|
1181
|
+
expr = self.parse_primary()
|
|
1182
|
+
|
|
1183
|
+
# Block-level expressions (switch/if) already consumed their DEDENT;
|
|
1184
|
+
# don't greedily attach postfix [ or . from the NEXT statement
|
|
1185
|
+
if isinstance(expr, (SwitchExpression, IfExpression)):
|
|
1186
|
+
return expr
|
|
1187
|
+
|
|
1188
|
+
while True:
|
|
1189
|
+
# Function call (may have generic parameters)
|
|
1190
|
+
# Parse as generics only for function/method names, not after index access
|
|
1191
|
+
# This prevents `arr[0] < 5` and `close < ema` from being parsed as generics
|
|
1192
|
+
# Heuristic: generics MUST be followed by ( for function call
|
|
1193
|
+
if (self.match(TokenType.LT) and
|
|
1194
|
+
self.peek_token().type in (TokenType.TYPE_IDENTIFIER, TokenType.IDENTIFIER) and
|
|
1195
|
+
isinstance(expr, (Identifier, MemberAccess)) and
|
|
1196
|
+
self._looks_like_generic_call()): # Additional check
|
|
1197
|
+
# Generic function call: func<T>() or obj.method<T>()
|
|
1198
|
+
generics = self.parse_generic_params()
|
|
1199
|
+
# Add generics to the expression (store as string annotation)
|
|
1200
|
+
if isinstance(expr, Identifier):
|
|
1201
|
+
expr.name = f"{expr.name}<{generics}>"
|
|
1202
|
+
elif isinstance(expr, MemberAccess):
|
|
1203
|
+
expr.member = f"{expr.member}<{generics}>"
|
|
1204
|
+
# Continue to parse the actual function call
|
|
1205
|
+
if self.match(TokenType.LPAREN):
|
|
1206
|
+
expr = self.parse_function_call(expr)
|
|
1207
|
+
elif self.match(TokenType.LPAREN):
|
|
1208
|
+
expr = self.parse_function_call(expr)
|
|
1209
|
+
# Member access
|
|
1210
|
+
elif self.match(TokenType.DOT):
|
|
1211
|
+
self.advance()
|
|
1212
|
+
member = self.expect(TokenType.IDENTIFIER).value
|
|
1213
|
+
|
|
1214
|
+
# Check for generic parameters after member (support both built-in and user-defined types)
|
|
1215
|
+
if (self.match(TokenType.LT) and
|
|
1216
|
+
self.peek_token().type in (TokenType.TYPE_IDENTIFIER, TokenType.IDENTIFIER) and
|
|
1217
|
+
self._looks_like_generic_call()):
|
|
1218
|
+
generics = self.parse_generic_params()
|
|
1219
|
+
member = f"{member}<{generics}>"
|
|
1220
|
+
|
|
1221
|
+
# Check if this is a method call
|
|
1222
|
+
if self.match(TokenType.LPAREN):
|
|
1223
|
+
expr = self.parse_method_call(expr, member)
|
|
1224
|
+
else:
|
|
1225
|
+
expr = MemberAccess(object=expr, member=member)
|
|
1226
|
+
# Index access
|
|
1227
|
+
elif self.match(TokenType.LBRACKET):
|
|
1228
|
+
self.advance()
|
|
1229
|
+
index = self.parse_expression()
|
|
1230
|
+
self.expect(TokenType.RBRACKET)
|
|
1231
|
+
expr = IndexAccess(object=expr, index=index)
|
|
1232
|
+
else:
|
|
1233
|
+
break
|
|
1234
|
+
|
|
1235
|
+
return expr
|
|
1236
|
+
|
|
1237
|
+
def parse_generic_params(self) -> str:
|
|
1238
|
+
"""Parse generic parameters in angle brackets: <T> or <K, V>."""
|
|
1239
|
+
self.expect(TokenType.LT) # consume <
|
|
1240
|
+
|
|
1241
|
+
params = []
|
|
1242
|
+
while not self.match(TokenType.GT):
|
|
1243
|
+
if self.match(TokenType.COMMA):
|
|
1244
|
+
self.advance()
|
|
1245
|
+
continue
|
|
1246
|
+
|
|
1247
|
+
# Parse type parameter
|
|
1248
|
+
if self.match(TokenType.TYPE_IDENTIFIER, TokenType.IDENTIFIER):
|
|
1249
|
+
params.append(self.advance().value)
|
|
1250
|
+
else:
|
|
1251
|
+
raise self.error(f"Expected type parameter, got {self.current_token().type.name}")
|
|
1252
|
+
|
|
1253
|
+
self.expect(TokenType.GT) # consume >
|
|
1254
|
+
|
|
1255
|
+
return ', '.join(params)
|
|
1256
|
+
|
|
1257
|
+
def parse_function_call(self, func: Expression) -> FunctionCall:
|
|
1258
|
+
"""Parse function call arguments."""
|
|
1259
|
+
self.expect(TokenType.LPAREN)
|
|
1260
|
+
|
|
1261
|
+
args = []
|
|
1262
|
+
kwargs = {}
|
|
1263
|
+
|
|
1264
|
+
while not self.match(TokenType.RPAREN):
|
|
1265
|
+
if self.match(TokenType.COMMA):
|
|
1266
|
+
self.advance()
|
|
1267
|
+
continue
|
|
1268
|
+
|
|
1269
|
+
self.skip_newlines()
|
|
1270
|
+
if self.match(TokenType.RPAREN):
|
|
1271
|
+
break
|
|
1272
|
+
|
|
1273
|
+
# Check for keyword argument (can be IDENTIFIER or TYPE_IDENTIFIER like 'color')
|
|
1274
|
+
if self.match(TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER) and self.peek_token().type == TokenType.ASSIGN:
|
|
1275
|
+
key = self.advance().value
|
|
1276
|
+
self.advance() # =
|
|
1277
|
+
value = self.parse_expression()
|
|
1278
|
+
kwargs[key] = value
|
|
1279
|
+
else:
|
|
1280
|
+
# Positional argument
|
|
1281
|
+
args.append(self.parse_expression())
|
|
1282
|
+
|
|
1283
|
+
self.expect(TokenType.RPAREN)
|
|
1284
|
+
|
|
1285
|
+
# Convert func expression to string if it's an identifier or member access
|
|
1286
|
+
if isinstance(func, Identifier):
|
|
1287
|
+
func_name = func.name
|
|
1288
|
+
elif isinstance(func, MemberAccess):
|
|
1289
|
+
func_name = func
|
|
1290
|
+
else:
|
|
1291
|
+
func_name = func
|
|
1292
|
+
|
|
1293
|
+
return FunctionCall(func=func_name, args=args, kwargs=kwargs)
|
|
1294
|
+
|
|
1295
|
+
def parse_method_call(self, obj: Expression, method: str) -> MethodCall:
|
|
1296
|
+
"""Parse method call arguments."""
|
|
1297
|
+
self.expect(TokenType.LPAREN)
|
|
1298
|
+
|
|
1299
|
+
args = []
|
|
1300
|
+
kwargs = {}
|
|
1301
|
+
|
|
1302
|
+
while not self.match(TokenType.RPAREN):
|
|
1303
|
+
if self.match(TokenType.COMMA):
|
|
1304
|
+
self.advance()
|
|
1305
|
+
continue
|
|
1306
|
+
|
|
1307
|
+
# Check for keyword argument (can be IDENTIFIER or TYPE_IDENTIFIER like 'color')
|
|
1308
|
+
if self.match(TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER) and self.peek_token().type == TokenType.ASSIGN:
|
|
1309
|
+
key = self.advance().value
|
|
1310
|
+
self.advance() # =
|
|
1311
|
+
value = self.parse_expression()
|
|
1312
|
+
kwargs[key] = value
|
|
1313
|
+
else:
|
|
1314
|
+
# Positional argument
|
|
1315
|
+
args.append(self.parse_expression())
|
|
1316
|
+
|
|
1317
|
+
self.expect(TokenType.RPAREN)
|
|
1318
|
+
|
|
1319
|
+
return MethodCall(object=obj, method=method, args=args, kwargs=kwargs)
|
|
1320
|
+
|
|
1321
|
+
def parse_primary(self) -> Expression:
|
|
1322
|
+
"""Parse primary expressions (literals, identifiers, parenthesized expressions)."""
|
|
1323
|
+
# Literals
|
|
1324
|
+
if self.match(TokenType.INT_LITERAL):
|
|
1325
|
+
token = self.advance()
|
|
1326
|
+
return Literal(value=token.value, literal_type='int')
|
|
1327
|
+
|
|
1328
|
+
if self.match(TokenType.FLOAT_LITERAL):
|
|
1329
|
+
token = self.advance()
|
|
1330
|
+
return Literal(value=token.value, literal_type='float')
|
|
1331
|
+
|
|
1332
|
+
if self.match(TokenType.STRING_LITERAL):
|
|
1333
|
+
token = self.advance()
|
|
1334
|
+
return Literal(value=token.value, literal_type='string', is_double_quoted=token.is_double_quoted)
|
|
1335
|
+
|
|
1336
|
+
if self.match(TokenType.TRUE):
|
|
1337
|
+
self.advance()
|
|
1338
|
+
return Literal(value=True, literal_type='bool')
|
|
1339
|
+
|
|
1340
|
+
if self.match(TokenType.FALSE):
|
|
1341
|
+
self.advance()
|
|
1342
|
+
return Literal(value=False, literal_type='bool')
|
|
1343
|
+
|
|
1344
|
+
if self.match(TokenType.COLOR_LITERAL):
|
|
1345
|
+
token = self.advance()
|
|
1346
|
+
return Literal(value=token.value, literal_type='color', is_double_quoted=token.is_double_quoted)
|
|
1347
|
+
|
|
1348
|
+
if self.match(TokenType.NA_LITERAL):
|
|
1349
|
+
self.advance()
|
|
1350
|
+
return NaLiteral()
|
|
1351
|
+
|
|
1352
|
+
# Array literal
|
|
1353
|
+
if self.match(TokenType.LBRACKET):
|
|
1354
|
+
self.advance()
|
|
1355
|
+
elements = []
|
|
1356
|
+
|
|
1357
|
+
while not self.match(TokenType.RBRACKET):
|
|
1358
|
+
if self.match(TokenType.COMMA):
|
|
1359
|
+
self.advance()
|
|
1360
|
+
continue
|
|
1361
|
+
|
|
1362
|
+
elements.append(self.parse_expression())
|
|
1363
|
+
|
|
1364
|
+
self.expect(TokenType.RBRACKET)
|
|
1365
|
+
return ArrayLiteral(elements=elements)
|
|
1366
|
+
|
|
1367
|
+
# Parenthesized expression
|
|
1368
|
+
if self.match(TokenType.LPAREN):
|
|
1369
|
+
self.advance()
|
|
1370
|
+
expr = self.parse_expression()
|
|
1371
|
+
self.expect(TokenType.RPAREN)
|
|
1372
|
+
return expr
|
|
1373
|
+
|
|
1374
|
+
# Identifier
|
|
1375
|
+
if self.match(TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER):
|
|
1376
|
+
token = self.advance()
|
|
1377
|
+
return Identifier(name=token.value)
|
|
1378
|
+
|
|
1379
|
+
# If expression
|
|
1380
|
+
if self.match(TokenType.IF):
|
|
1381
|
+
return self.parse_if_expression()
|
|
1382
|
+
|
|
1383
|
+
# Switch expression
|
|
1384
|
+
if self.match(TokenType.SWITCH):
|
|
1385
|
+
return self.parse_switch_expression()
|
|
1386
|
+
|
|
1387
|
+
# For/for-in as value expression: x = for i in arr ...
|
|
1388
|
+
if self.match(TokenType.FOR):
|
|
1389
|
+
return self.parse_for_loop()
|
|
1390
|
+
|
|
1391
|
+
raise self.error(f"Unexpected token in expression: {self.current_token().type.name}")
|
|
1392
|
+
|
|
1393
|
+
def parse_if_expression(self) -> IfExpression:
|
|
1394
|
+
"""Parse if expression (returns a value)."""
|
|
1395
|
+
self.expect(TokenType.IF)
|
|
1396
|
+
condition = self.parse_expression()
|
|
1397
|
+
|
|
1398
|
+
self.skip_newlines()
|
|
1399
|
+
self.expect(TokenType.INDENT)
|
|
1400
|
+
|
|
1401
|
+
# Parse true branch (can be expression or statements)
|
|
1402
|
+
true_expr = self.parse_expression_or_block()
|
|
1403
|
+
|
|
1404
|
+
self.skip_newlines()
|
|
1405
|
+
self.expect(TokenType.DEDENT)
|
|
1406
|
+
|
|
1407
|
+
# Parse else/else-if branch
|
|
1408
|
+
false_expr = None
|
|
1409
|
+
if self.match(TokenType.ELSE):
|
|
1410
|
+
self.advance()
|
|
1411
|
+
self.skip_newlines()
|
|
1412
|
+
if self.match(TokenType.IF):
|
|
1413
|
+
# else if → recurse into another if-expression
|
|
1414
|
+
false_expr = self.parse_if_expression()
|
|
1415
|
+
else:
|
|
1416
|
+
self.expect(TokenType.INDENT)
|
|
1417
|
+
false_expr = self.parse_expression_or_block()
|
|
1418
|
+
self.skip_newlines()
|
|
1419
|
+
self.expect(TokenType.DEDENT)
|
|
1420
|
+
|
|
1421
|
+
return IfExpression(condition=condition, true_expr=true_expr, false_expr=false_expr)
|
|
1422
|
+
|
|
1423
|
+
def parse_switch_expression(self) -> SwitchExpression:
|
|
1424
|
+
"""Parse switch expression (returns a value)."""
|
|
1425
|
+
self.expect(TokenType.SWITCH)
|
|
1426
|
+
|
|
1427
|
+
# Optional expression to switch on
|
|
1428
|
+
expr = None
|
|
1429
|
+
if not self.match(TokenType.NEWLINE):
|
|
1430
|
+
expr = self.parse_expression()
|
|
1431
|
+
|
|
1432
|
+
self.skip_newlines()
|
|
1433
|
+
self.expect(TokenType.INDENT)
|
|
1434
|
+
|
|
1435
|
+
cases = []
|
|
1436
|
+
default = None
|
|
1437
|
+
|
|
1438
|
+
while not self.match(TokenType.DEDENT):
|
|
1439
|
+
self.skip_newlines()
|
|
1440
|
+
if self.match(TokenType.DEDENT):
|
|
1441
|
+
break
|
|
1442
|
+
|
|
1443
|
+
# Check for default case (starts with =>)
|
|
1444
|
+
if self.match(TokenType.ARROW):
|
|
1445
|
+
self.advance() # consume =>
|
|
1446
|
+
self.skip_newlines()
|
|
1447
|
+
# Parse default value — may be a multi-statement block
|
|
1448
|
+
if self.match(TokenType.INDENT):
|
|
1449
|
+
self.advance() # consume INDENT
|
|
1450
|
+
default = self.parse_expression_or_block()
|
|
1451
|
+
self.skip_newlines()
|
|
1452
|
+
self.expect(TokenType.DEDENT)
|
|
1453
|
+
else:
|
|
1454
|
+
default = self.parse_expression()
|
|
1455
|
+
self.skip_newlines()
|
|
1456
|
+
continue
|
|
1457
|
+
|
|
1458
|
+
# Parse case condition
|
|
1459
|
+
case_condition = self.parse_expression()
|
|
1460
|
+
|
|
1461
|
+
self.expect(TokenType.ARROW)
|
|
1462
|
+
self.skip_newlines()
|
|
1463
|
+
|
|
1464
|
+
# Parse case value — may be a multi-statement block
|
|
1465
|
+
if self.match(TokenType.INDENT):
|
|
1466
|
+
self.advance() # consume INDENT
|
|
1467
|
+
case_value = self.parse_expression_or_block()
|
|
1468
|
+
self.skip_newlines()
|
|
1469
|
+
self.expect(TokenType.DEDENT)
|
|
1470
|
+
else:
|
|
1471
|
+
case_value = self.parse_expression()
|
|
1472
|
+
|
|
1473
|
+
cases.append((case_condition, case_value))
|
|
1474
|
+
self.skip_newlines()
|
|
1475
|
+
|
|
1476
|
+
self.expect(TokenType.DEDENT)
|
|
1477
|
+
|
|
1478
|
+
return SwitchExpression(expr=expr, cases=cases, default=default)
|
|
1479
|
+
|
|
1480
|
+
def parse_expression_or_block(self) -> Union[Expression, List[Statement]]:
|
|
1481
|
+
"""Parse either a single expression or a block of statements.
|
|
1482
|
+
For multi-statement blocks, returns a list of statements where the
|
|
1483
|
+
last expression is the return value.
|
|
1484
|
+
"""
|
|
1485
|
+
# Try to parse as expression first
|
|
1486
|
+
start_pos = self.pos
|
|
1487
|
+
try:
|
|
1488
|
+
expr = self.parse_expression()
|
|
1489
|
+
# If next is DEDENT, it's a single expression (end of block)
|
|
1490
|
+
if self.match(TokenType.DEDENT):
|
|
1491
|
+
return expr
|
|
1492
|
+
# If next is NEWLINE, check if more statements follow
|
|
1493
|
+
if self.match(TokenType.NEWLINE):
|
|
1494
|
+
self.skip_newlines()
|
|
1495
|
+
# If DEDENT follows the newlines, it's a single expression
|
|
1496
|
+
if self.match(TokenType.DEDENT):
|
|
1497
|
+
return expr
|
|
1498
|
+
# Otherwise there are more statements — fall through to block parse
|
|
1499
|
+
except:
|
|
1500
|
+
pass
|
|
1501
|
+
|
|
1502
|
+
# Otherwise, parse as block
|
|
1503
|
+
self.pos = start_pos
|
|
1504
|
+
return self.parse_block()
|
|
1505
|
+
|
|
1506
|
+
|
|
1507
|
+
def parse(tokens: List[Token]) -> Script:
|
|
1508
|
+
"""Convenience function to parse Pine Script tokens into AST."""
|
|
1509
|
+
parser = Parser(tokens)
|
|
1510
|
+
return parser.parse()
|