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.
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()