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.
@@ -0,0 +1,2327 @@
1
+ """
2
+ AST Transformer: Pine Script AST → PyneCore-compatible AST.
3
+
4
+ Implements all 54 transformation rules from the transpiler specification.
5
+ """
6
+ from typing import List, Dict, Any, Union, Optional
7
+ from .ast_nodes import *
8
+ from .symbol_table import SymbolTable, Symbol, VariableKind
9
+ from .type_inference import TypeInference
10
+ from .import_resolver import ImportResolver
11
+ from .pine_builtins import (
12
+ get_function_name, get_module_name, get_type_name,
13
+ get_plot_style_remap, needs_plot_style_remap,
14
+ is_input_function, TYPE_RENAMES, PYNECORE_LIB_MODULES,
15
+ LABEL_METHODS, LINE_METHODS, BOX_METHODS, TABLE_METHODS,
16
+ MAP_METHODS, MATRIX_METHODS, ARRAY_METHODS, MAP_UNIQUE_METHODS,
17
+ SHARED_COLLECTION_METHODS,
18
+ )
19
+
20
+ # Known module names - used to guard against transforming module.function() calls
21
+ # as if they were chained member access (e.g. array.size != obj.size)
22
+ KNOWN_MODULES = PYNECORE_LIB_MODULES | {
23
+ 'bar_index', 'close', 'open', 'high', 'low', 'volume', 'time',
24
+ 'hl2', 'hlc3', 'ohlc4', 'hlcc4',
25
+ 'dayofmonth', 'dayofweek', 'hour', 'minute', 'month', 'second',
26
+ 'weekofyear', 'year', 'timenow',
27
+ 'adjustment', 'earnings', 'dividends', 'splits',
28
+ 'order', 'str',
29
+ }
30
+ from .errors import TransformerError, UnsupportedFeatureError
31
+
32
+
33
+ class PyneTransformedScript:
34
+ """Holds the transformed script ready for code generation."""
35
+
36
+ def __init__(self):
37
+ self.imports: List[str] = []
38
+ self.type_declarations: List[TypeDecl] = [] # Custom type definitions
39
+ self.enum_declarations: List[EnumDecl] = [] # Enum definitions
40
+ self.helper_functions: List[FuncDecl] = []
41
+ self.main_decorator: Optional[Union[IndicatorDecl, StrategyDecl]] = None
42
+ self.main_params: List[Parameter] = []
43
+ self.main_body: List[Statement] = []
44
+ self.global_vars: List[Union[VarDecl, Assignment]] = []
45
+ self.module_constants: List[Assignment] = [] # Pre-main literal constants
46
+ self.cache_slots: dict[int, tuple[str, frozenset[str]]] = {} # slot → (varname, deps)
47
+ self.num_cache_slots: int = 0
48
+
49
+
50
+ def convert_pine_generic_to_python(pine_type: str) -> str:
51
+ """
52
+ Convert Pine Script generic types to Python type hints.
53
+
54
+ Examples:
55
+ map<string, float> -> dict[str, float]
56
+ array<int> -> list[int]
57
+ """
58
+ # Check if it contains generic syntax
59
+ if '<' not in pine_type or '>' not in pine_type:
60
+ return get_type_name(pine_type)
61
+
62
+ # Extract base type and parameters
63
+ if '<' in pine_type:
64
+ base_type = pine_type[:pine_type.index('<')]
65
+ params_str = pine_type[pine_type.index('<')+1:pine_type.rindex('>')]
66
+ params = [p.strip() for p in params_str.split(',')]
67
+
68
+ # Convert base type
69
+ if base_type == 'map':
70
+ base_type = 'dict'
71
+ elif base_type == 'array':
72
+ base_type = 'list'
73
+ else:
74
+ # For other types like matrix, line, etc., use type name mapping
75
+ base_type = get_type_name(base_type)
76
+
77
+ # Convert parameter types
78
+ converted_params = [get_type_name(p) for p in params]
79
+
80
+ return f"{base_type}[{', '.join(converted_params)}]"
81
+
82
+ return get_type_name(pine_type)
83
+
84
+
85
+ class Transformer:
86
+ """Transforms Pine Script AST to PyneCore-compatible form."""
87
+
88
+ def __init__(self):
89
+ self.symbol_table = SymbolTable()
90
+ self.type_inference = TypeInference(self.symbol_table)
91
+ self.import_resolver = ImportResolver()
92
+ self.input_declarations: List[InputDecl] = []
93
+ self.current_function_params: Dict[str, str] = {} # Track current function parameters
94
+ self.method_functions: set = set() # Track @method decorated function names
95
+ self._renamed_functions: Dict[str, str] = {} # Pine name -> sanitized Python name
96
+
97
+ # PRIORITY 1 FIX: Track Series variables in user-defined functions
98
+ self.in_user_function = False # Are we inside a user-defined function?
99
+ self.series_variables: set = set() # Variables that need to be Series objects
100
+ self.function_local_vars: set = set() # All local variables in current function
101
+
102
+ def _get_module_references_in_expr(self, expr: Optional[Expression]) -> set[str]:
103
+ """
104
+ Recursively find all module references in an expression.
105
+ Returns set of module names that are referenced (e.g., 'position' in position.middle_center).
106
+ """
107
+ if expr is None:
108
+ return set()
109
+
110
+ modules = set()
111
+
112
+ # Handle Identifier with dotted names (e.g., 'position.middle_center')
113
+ if isinstance(expr, Identifier):
114
+ if '.' in expr.name:
115
+ module = expr.name.split('.')[0]
116
+ if module in KNOWN_MODULES or module in self.import_resolver.lib_modules:
117
+ modules.add(module)
118
+
119
+ # Handle MemberAccess nodes
120
+ if isinstance(expr, MemberAccess):
121
+ # Handle module.member access
122
+ if isinstance(expr.object, str):
123
+ if expr.object in KNOWN_MODULES or expr.object in self.import_resolver.lib_modules:
124
+ modules.add(expr.object)
125
+ elif isinstance(expr.object, Identifier):
126
+ if expr.object.name in KNOWN_MODULES or expr.object.name in self.import_resolver.lib_modules:
127
+ modules.add(expr.object.name)
128
+
129
+ # Recursively check nested expressions
130
+ if hasattr(expr, '__dict__'):
131
+ for attr_value in expr.__dict__.values():
132
+ if isinstance(attr_value, (ASTNode, Expression)):
133
+ modules.update(self._get_module_references_in_expr(attr_value))
134
+ elif isinstance(attr_value, list):
135
+ for item in attr_value:
136
+ if isinstance(item, (ASTNode, Expression)):
137
+ modules.update(self._get_module_references_in_expr(item))
138
+
139
+ return modules
140
+
141
+ def _rewrite_module_references(self, expr: Optional[Expression]) -> Optional[Expression]:
142
+ """
143
+ Rewrite an expression to use module aliases where needed.
144
+ Replaces module references with their aliased versions (e.g., position -> _position_module).
145
+ """
146
+ if expr is None:
147
+ return None
148
+
149
+ # Handle Identifier with dotted names (e.g., 'position.middle_center' -> '_position_module.middle_center')
150
+ if isinstance(expr, Identifier):
151
+ if '.' in expr.name:
152
+ module, rest = expr.name.split('.', 1)
153
+ if module in self.import_resolver.aliased_modules:
154
+ alias = self.import_resolver.aliased_modules[module]
155
+ return Identifier(name=f"{alias}.{rest}")
156
+
157
+ # Handle MemberAccess nodes
158
+ if isinstance(expr, MemberAccess):
159
+ # Rewrite module.member to alias.member if module is aliased
160
+ if isinstance(expr.object, str):
161
+ if expr.object in self.import_resolver.aliased_modules:
162
+ return MemberAccess(
163
+ object=self.import_resolver.aliased_modules[expr.object],
164
+ member=expr.member
165
+ )
166
+ elif isinstance(expr.object, Identifier):
167
+ if expr.object.name in self.import_resolver.aliased_modules:
168
+ return MemberAccess(
169
+ object=self.import_resolver.aliased_modules[expr.object.name],
170
+ member=expr.member
171
+ )
172
+
173
+ # Recursively rewrite nested expressions
174
+ if hasattr(expr, '__dict__'):
175
+ new_dict = {}
176
+ for attr_name, attr_value in expr.__dict__.items():
177
+ if isinstance(attr_value, (ASTNode, Expression)):
178
+ new_dict[attr_name] = self._rewrite_module_references(attr_value)
179
+ elif isinstance(attr_value, list):
180
+ new_dict[attr_name] = [
181
+ self._rewrite_module_references(item) if isinstance(item, (ASTNode, Expression)) else item
182
+ for item in attr_value
183
+ ]
184
+ else:
185
+ new_dict[attr_name] = attr_value
186
+
187
+ # Create new instance with rewritten attributes
188
+ return type(expr)(**new_dict)
189
+
190
+ return expr
191
+
192
+ def _visit_ast_nodes(self, nodes: list, visitor_fn) -> None:
193
+ """Walk all AST nodes depth-first, calling visitor_fn on each."""
194
+ def visit(node):
195
+ visitor_fn(node)
196
+ if not hasattr(node, '__dataclass_fields__'):
197
+ return
198
+ for field_name in node.__dataclass_fields__:
199
+ val = getattr(node, field_name)
200
+ if hasattr(val, '__dataclass_fields__'):
201
+ visit(val)
202
+ elif isinstance(val, list):
203
+ for item in val:
204
+ if hasattr(item, '__dataclass_fields__'):
205
+ visit(item)
206
+ elif isinstance(val, dict):
207
+ for v in val.values():
208
+ if hasattr(v, '__dataclass_fields__'):
209
+ visit(v)
210
+
211
+ for node in nodes:
212
+ visit(node)
213
+
214
+ def _collect_reassignment_vars(self, script: Script) -> set:
215
+ """Collect variable names that are reassigned with := operator."""
216
+ reassign_vars = set()
217
+
218
+ def visitor(node):
219
+ if isinstance(node, Reassignment):
220
+ reassign_vars.add(node.target)
221
+
222
+ self._visit_ast_nodes(list(script.declarations) + list(script.body), visitor)
223
+ return reassign_vars
224
+
225
+ def _find_series_variables_in_function(self, func_body: List[Statement]) -> set:
226
+ """Find variables in a function that need to be Series objects.
227
+ A variable needs to be Series if it's reassigned with := or indexed with [n].
228
+ """
229
+ series_vars = set()
230
+
231
+ def visitor(node):
232
+ if isinstance(node, Reassignment):
233
+ series_vars.add(node.target)
234
+ if isinstance(node, IndexAccess):
235
+ if isinstance(node.object, Identifier):
236
+ series_vars.add(node.object.name)
237
+
238
+ self._visit_ast_nodes(func_body, visitor)
239
+ return series_vars
240
+
241
+ def transform(self, script: Script) -> PyneTransformedScript:
242
+ """Transform entire script."""
243
+ output = PyneTransformedScript()
244
+
245
+ # Build symbol table
246
+ self._build_symbol_table(script)
247
+
248
+ # Infer types
249
+ self.type_inference.infer_types(script)
250
+
251
+ # (Removed: _history_ref_vars replaced by symbol.is_indexed from type inference)
252
+
253
+ # Collect variables that use := (reassignment)
254
+ self._reassignment_vars = self._collect_reassignment_vars(script)
255
+
256
+ # Analyze imports
257
+ self.import_resolver.analyze(script)
258
+
259
+ # Transform script declaration (indicator/strategy)
260
+ if script.script_decl:
261
+ output.main_decorator = self._transform_script_declaration(script.script_decl)
262
+
263
+ # Extract input declarations to main() parameters
264
+ self._extract_input_declarations(script)
265
+ output.main_params = self._create_main_parameters()
266
+
267
+ # Transform type and enum declarations - stay at module level
268
+ for decl in script.declarations:
269
+ if isinstance(decl, TypeDecl):
270
+ output.type_declarations.append(decl) # Keep as-is for codegen
271
+ elif isinstance(decl, EnumDecl):
272
+ output.enum_declarations.append(decl) # Keep as-is for codegen
273
+
274
+ # Identify body assignments that must be module-level constants.
275
+ # In Pine Script, declarations before the first function call (like
276
+ # `const string G_ENTRY = '...'` or `DEFAULT_PYRAMIDING = 6`) can be
277
+ # referenced by input.* group= params and strategy() kwargs, which
278
+ # become main() parameter defaults evaluated at function definition time.
279
+ # These must be emitted at module level, not inside main().
280
+ module_const_names = self._collect_module_constant_names(script)
281
+
282
+ # Merge all non-type, non-input declarations with body statements
283
+ # sorted by source line number to preserve original Pine Script ordering
284
+ all_items: List[tuple] = []
285
+ for decl in script.declarations:
286
+ if isinstance(decl, (VarDecl, VaripDecl)):
287
+ all_items.append(('var', decl))
288
+ elif isinstance(decl, FuncDecl):
289
+ all_items.append(('func', decl))
290
+ # TypeDecl and InputDecl are handled separately above
291
+
292
+ for stmt in script.body:
293
+ all_items.append(('body', stmt))
294
+
295
+ # Sort by line number to preserve source order
296
+ all_items.sort(key=lambda x: x[1].line)
297
+
298
+ # Process all items in source order into main_body
299
+ # Propagate source line numbers for blank line detection in codegen
300
+ for item_type, item in all_items:
301
+ source_line = item.line
302
+ if item_type == 'var':
303
+ # Check if this var declaration should be a module-level constant
304
+ if (isinstance(item, (VarDecl, VaripDecl)) and
305
+ item.name in module_const_names and
306
+ isinstance(item.value, Literal)):
307
+ # Emit as simple module-level assignment (not Persistent)
308
+ transformed = self._transform_statement(
309
+ Assignment(target=item.name, value=item.value,
310
+ type_hint=item.type_hint))
311
+ if transformed and not isinstance(transformed, list):
312
+ transformed.line = source_line
313
+ output.module_constants.append(transformed)
314
+ continue
315
+ result = self._transform_var_declaration(item)
316
+ result.line = source_line
317
+ output.main_body.append(result)
318
+ elif item_type == 'func':
319
+ result = self._transform_function(item)
320
+ result.line = source_line
321
+ output.main_body.append(result)
322
+ else: # body
323
+ # Check if this is a literal assignment that should be module-level
324
+ if (isinstance(item, Assignment) and
325
+ isinstance(item.target, str) and
326
+ item.target in module_const_names and
327
+ isinstance(item.value, Literal)):
328
+ # Emit as module-level constant instead of inside main()
329
+ transformed = self._transform_statement(item)
330
+ if transformed and not isinstance(transformed, list):
331
+ transformed.line = source_line
332
+ output.module_constants.append(transformed)
333
+ continue
334
+
335
+ transformed = self._transform_statement(item)
336
+ if transformed:
337
+ if isinstance(transformed, list):
338
+ for t in transformed:
339
+ t.line = source_line
340
+ output.main_body.extend(transformed)
341
+ else:
342
+ transformed.line = source_line
343
+ output.main_body.append(transformed)
344
+
345
+ # Insert Series wrapper assignments for input.source parameters at body start
346
+ # These allow [n] subscript access on source inputs
347
+ if hasattr(self, '_source_input_wrappers'):
348
+ for orig_name, src_param_name in reversed(self._source_input_wrappers):
349
+ wrapper = Assignment(
350
+ target=orig_name,
351
+ value=Identifier(name=src_param_name),
352
+ type_hint='Series[float]'
353
+ )
354
+ wrapper.line = 0 # Place at beginning
355
+ output.main_body.insert(0, wrapper)
356
+
357
+ # Resolve function overloading (multiple defs with same name)
358
+ self._resolve_overloaded_functions(output.main_body)
359
+
360
+ # Analyze variable cache dependencies for optimize mode
361
+ self._analyze_cache_deps(output)
362
+
363
+ # Analyze transformed AST for type names in type hints (Matrix, Line, etc.)
364
+ self._analyze_transformed_types(output)
365
+
366
+ # If variable cache is used, bar_index must be imported (used in _vc[N][int(bar_index)])
367
+ if output.num_cache_slots > 0:
368
+ self.import_resolver.builtin_variables.add('bar_index')
369
+
370
+ # Generate imports
371
+ output.imports = self.import_resolver.generate_imports()
372
+
373
+ return output
374
+
375
+ # ------------------------------------------------------------------
376
+ # Variable cache dependency analysis (for pyne optimize)
377
+ # ------------------------------------------------------------------
378
+
379
+ # OHLCV builtins — always available, no input dependency
380
+ _OHLCV_BUILTINS = frozenset({
381
+ 'close', 'open', 'high', 'low', 'volume',
382
+ 'hl2', 'hlc3', 'ohlc4', 'hlcc4',
383
+ 'time', 'time_close', 'bar_index',
384
+ })
385
+
386
+ # Safe module references — accessing their members introduces no dependency
387
+ _SAFE_MODULES = frozenset({
388
+ 'ta', 'math', 'color', 'str', 'array', 'map', 'nz', 'na',
389
+ 'timeframe', 'syminfo', 'chart',
390
+ })
391
+
392
+ # Modules whose state depends on trading — always unknown
393
+ _TRADING_STATE_MODULES = frozenset({'strategy', 'position'})
394
+
395
+ def _analyze_cache_deps(self, output: 'PyneTransformedScript') -> None:
396
+ """Analyze which main_body assignments can be cached in optimize mode.
397
+
398
+ For each top-level assignment of a function call result, compute its
399
+ transitive input-parameter dependencies. Assignments whose deps are
400
+ a subset of non-optimized params (or empty) can be cached across
401
+ optimizer runs.
402
+ """
403
+ # 1. Collect input param names
404
+ input_params: set[str] = set()
405
+ for p in output.main_params:
406
+ input_params.add(p.name)
407
+
408
+ # 2. Build initial deps_map: OHLCV builtins → empty, params → {name}
409
+ deps_map: dict[str, frozenset[str]] = {}
410
+ for name in self._OHLCV_BUILTINS:
411
+ deps_map[name] = frozenset()
412
+ for name in input_params:
413
+ deps_map[name] = frozenset({name})
414
+
415
+ # 3. Pre-scan: find targets assigned more than once or inside control flow
416
+ reassigned: set[str] = set()
417
+ control_assigned: set[str] = set()
418
+ assign_count: dict[str, int] = {}
419
+
420
+ def _count_assigns(stmts):
421
+ for s in stmts:
422
+ if isinstance(s, Assignment) and isinstance(s.target, str):
423
+ assign_count[s.target] = assign_count.get(s.target, 0) + 1
424
+ elif isinstance(s, Reassignment) and isinstance(s.target, str):
425
+ reassigned.add(s.target)
426
+
427
+ def _scan_control_flow(stmts):
428
+ for s in stmts:
429
+ if isinstance(s, (IfStatement, ForLoop, ForInLoop, WhileLoop)):
430
+ bodies = []
431
+ if isinstance(s, IfStatement):
432
+ bodies.append(s.body)
433
+ for _, elif_body in s.elseifs:
434
+ bodies.append(elif_body)
435
+ if s.else_body:
436
+ bodies.append(s.else_body)
437
+ elif isinstance(s, (ForLoop, ForInLoop, WhileLoop)):
438
+ bodies.append(s.body)
439
+ for body in bodies:
440
+ for bs in body:
441
+ if isinstance(bs, Assignment) and isinstance(bs.target, str):
442
+ control_assigned.add(bs.target)
443
+ elif isinstance(bs, Reassignment) and isinstance(bs.target, str):
444
+ control_assigned.add(bs.target)
445
+ _scan_control_flow(body)
446
+
447
+ _count_assigns(output.main_body)
448
+ _scan_control_flow(output.main_body)
449
+ multi_assigned = {k for k, v in assign_count.items() if v > 1}
450
+ skip_targets = multi_assigned | reassigned | control_assigned
451
+
452
+ # 4. Walk top-level assignments sequentially
453
+ slot_idx = 0
454
+ for stmt in output.main_body:
455
+ if not isinstance(stmt, Assignment):
456
+ continue
457
+ if isinstance(stmt.target, TupleDestructure):
458
+ continue
459
+ target = stmt.target
460
+ if not isinstance(target, str):
461
+ continue
462
+
463
+ # Skip Persistent/PersistentSeries
464
+ if stmt.type_hint and 'Persistent' in stmt.type_hint:
465
+ continue
466
+
467
+ # Skip reassigned / control-flow assigned
468
+ if target in skip_targets:
469
+ continue
470
+
471
+ # Only cache function call results
472
+ if not isinstance(stmt.value, (FunctionCall, MethodCall)):
473
+ # Still track deps for downstream use if it's a simple identifier
474
+ if isinstance(stmt.value, Identifier):
475
+ ref_deps = deps_map.get(stmt.value.name)
476
+ if ref_deps is not None:
477
+ deps_map[target] = ref_deps
478
+ continue
479
+
480
+ # Skip input.*() calls from caching, but track in deps_map
481
+ # so downstream variables can correctly resolve their deps
482
+ if isinstance(stmt.value, FunctionCall):
483
+ func = stmt.value.func
484
+ is_input = False
485
+ if isinstance(func, str) and func == 'input':
486
+ is_input = True
487
+ elif isinstance(func, MemberAccess):
488
+ if (isinstance(func.object, str) and func.object == 'input'):
489
+ is_input = True
490
+ elif (isinstance(func.object, Identifier) and func.object.name == 'input'):
491
+ is_input = True
492
+ if is_input:
493
+ deps_map[target] = frozenset({target})
494
+ continue
495
+
496
+ # Compute deps
497
+ deps = self._expr_deps(stmt.value, deps_map)
498
+ if '__UNKNOWN__' not in deps:
499
+ output.cache_slots[slot_idx] = (target, deps)
500
+ slot_idx += 1
501
+ deps_map[target] = deps
502
+ else:
503
+ # Track as unknown for downstream
504
+ deps_map[target] = frozenset({'__UNKNOWN__'})
505
+
506
+ output.num_cache_slots = slot_idx
507
+
508
+ def _expr_deps(
509
+ self,
510
+ expr,
511
+ deps_map: dict[str, frozenset[str]],
512
+ ) -> frozenset[str]:
513
+ """Recursively compute input-parameter dependencies of an expression."""
514
+ if expr is None:
515
+ return frozenset()
516
+
517
+ if isinstance(expr, (Literal, NaLiteral)):
518
+ return frozenset()
519
+
520
+ if isinstance(expr, Identifier):
521
+ return deps_map.get(expr.name, frozenset({'__UNKNOWN__'}))
522
+
523
+ if isinstance(expr, FunctionCall):
524
+ result: set[str] = set()
525
+ for arg in expr.args:
526
+ result |= self._expr_deps(arg, deps_map)
527
+ for val in expr.kwargs.values():
528
+ result |= self._expr_deps(val, deps_map)
529
+ return frozenset(result)
530
+
531
+ if isinstance(expr, MethodCall):
532
+ result = set(self._expr_deps(expr.object, deps_map))
533
+ for arg in expr.args:
534
+ result |= self._expr_deps(arg, deps_map)
535
+ for val in expr.kwargs.values():
536
+ result |= self._expr_deps(val, deps_map)
537
+ return frozenset(result)
538
+
539
+ if isinstance(expr, BinaryOp):
540
+ return self._expr_deps(expr.left, deps_map) | self._expr_deps(expr.right, deps_map)
541
+
542
+ if isinstance(expr, UnaryOp):
543
+ return self._expr_deps(expr.operand, deps_map)
544
+
545
+ if isinstance(expr, TernaryOp):
546
+ return (self._expr_deps(expr.condition, deps_map) |
547
+ self._expr_deps(expr.true_expr, deps_map) |
548
+ self._expr_deps(expr.false_expr, deps_map))
549
+
550
+ if isinstance(expr, MemberAccess):
551
+ obj = expr.object
552
+ if isinstance(obj, str):
553
+ if obj in self._SAFE_MODULES:
554
+ return frozenset()
555
+ if obj in self._TRADING_STATE_MODULES:
556
+ return frozenset({'__UNKNOWN__'})
557
+ if obj in self._OHLCV_BUILTINS:
558
+ return frozenset()
559
+ return deps_map.get(obj, frozenset({'__UNKNOWN__'}))
560
+ if isinstance(obj, Identifier):
561
+ if obj.name in self._SAFE_MODULES:
562
+ return frozenset()
563
+ if obj.name in self._TRADING_STATE_MODULES:
564
+ return frozenset({'__UNKNOWN__'})
565
+ if obj.name in self._OHLCV_BUILTINS:
566
+ return frozenset()
567
+ return deps_map.get(obj.name, frozenset({'__UNKNOWN__'}))
568
+ return self._expr_deps(obj, deps_map)
569
+
570
+ if isinstance(expr, IndexAccess):
571
+ return self._expr_deps(expr.object, deps_map) | self._expr_deps(expr.index, deps_map)
572
+
573
+ if isinstance(expr, ArrayLiteral):
574
+ result = set()
575
+ for elem in expr.elements:
576
+ result |= self._expr_deps(elem, deps_map)
577
+ return frozenset(result)
578
+
579
+ # Unknown node type → mark as unknown
580
+ return frozenset({'__UNKNOWN__'})
581
+
582
+ def _get_func_name_from_call(self, call: FunctionCall) -> Optional[str]:
583
+ """Extract function name string from a FunctionCall node."""
584
+ if isinstance(call.func, str):
585
+ return call.func
586
+ elif isinstance(call.func, MemberAccess):
587
+ if isinstance(call.func.object, str):
588
+ return f"{call.func.object}.{call.func.member}"
589
+ elif isinstance(call.func.object, Identifier):
590
+ return f"{call.func.object.name}.{call.func.member}"
591
+ return None
592
+
593
+ def _analyze_transformed_types(self, output: PyneTransformedScript) -> None:
594
+ """Analyze transformed AST to extract type names from type hints."""
595
+ import re
596
+
597
+ # Track if Series is actually used
598
+ series_used = False
599
+ persistent_used = False
600
+
601
+ def extract_types_from_hint(type_hint: str) -> None:
602
+ """Extract type names like Line, Matrix from type hints."""
603
+ nonlocal series_used, persistent_used
604
+
605
+ if not type_hint:
606
+ return
607
+
608
+ # Check for Series and Persistent usage
609
+ if 'Series[' in type_hint:
610
+ series_used = True
611
+ if 'Persistent[' in type_hint:
612
+ persistent_used = True
613
+
614
+ # Match capital letter type names that might be drawing/structural types
615
+ for match in re.finditer(r'\b([A-Z][a-zA-Z]*)\b', type_hint):
616
+ type_name = match.group(1)
617
+ # Add to import resolver if it's a known type
618
+ self.import_resolver.add_drawing_type(type_name)
619
+
620
+ def scan_statement(stmt: Statement) -> None:
621
+ """Recursively scan a statement for type hints."""
622
+ # Handle lists (flattened statement lists from hoisting)
623
+ if isinstance(stmt, list):
624
+ for s in stmt:
625
+ scan_statement(s)
626
+ return
627
+ if stmt is None:
628
+ return
629
+
630
+ # Check if statement has type hint
631
+ if hasattr(stmt, 'type_hint') and stmt.type_hint:
632
+ extract_types_from_hint(stmt.type_hint)
633
+
634
+ # Recursively scan nested structures
635
+ if isinstance(stmt, FuncDecl) and hasattr(stmt, 'params'):
636
+ for param in stmt.params:
637
+ if hasattr(param, 'type_hint') and param.type_hint:
638
+ extract_types_from_hint(param.type_hint)
639
+ if hasattr(stmt, 'body') and isinstance(stmt.body, list):
640
+ for body_stmt in stmt.body:
641
+ scan_statement(body_stmt)
642
+
643
+ # Scan all compound statement bodies
644
+ elif isinstance(stmt, IfStatement):
645
+ for body_stmt in (stmt.body or []):
646
+ scan_statement(body_stmt)
647
+ for _, elif_body in (stmt.elseifs or []):
648
+ for body_stmt in elif_body:
649
+ scan_statement(body_stmt)
650
+ for body_stmt in (stmt.else_body or []):
651
+ scan_statement(body_stmt)
652
+ elif isinstance(stmt, (ForLoop, ForInLoop, WhileLoop)):
653
+ for body_stmt in (stmt.body or []):
654
+ scan_statement(body_stmt)
655
+
656
+ # Scan global vars for type hints (var/varip declarations)
657
+ for var in output.global_vars:
658
+ if hasattr(var, 'type_hint') and var.type_hint:
659
+ extract_types_from_hint(var.type_hint)
660
+
661
+ # Scan main body for type hints
662
+ for stmt in output.main_body:
663
+ scan_statement(stmt)
664
+
665
+ # Scan main parameters for type hints
666
+ for param in output.main_params:
667
+ if hasattr(param, 'type_hint') and param.type_hint:
668
+ extract_types_from_hint(param.type_hint)
669
+
670
+ # Update import resolver based on actual usage in transformed code
671
+ self.import_resolver.uses_series = series_used
672
+ self.import_resolver.uses_persistent = persistent_used
673
+
674
+ # ========================================================================
675
+ # Symbol table building
676
+ # ========================================================================
677
+
678
+ def _build_symbol_table(self, script: Script) -> None:
679
+ """Build symbol table from script declarations."""
680
+ # Register input declarations
681
+ for decl in script.declarations:
682
+ if isinstance(decl, InputDecl):
683
+ symbol = Symbol(
684
+ decl.name,
685
+ VariableKind.INPUT,
686
+ is_global=True
687
+ )
688
+ self.symbol_table.define(symbol)
689
+
690
+ # Register var/varip declarations
691
+ for decl in script.declarations:
692
+ if isinstance(decl, VarDecl):
693
+ type_hint = self._infer_type_from_declaration(decl)
694
+ symbol = Symbol(
695
+ decl.name,
696
+ VariableKind.VAR,
697
+ type_hint=type_hint,
698
+ is_global=True
699
+ )
700
+ self.symbol_table.define(symbol)
701
+ elif isinstance(decl, VaripDecl):
702
+ type_hint = self._infer_type_from_declaration(decl)
703
+ symbol = Symbol(
704
+ decl.name,
705
+ VariableKind.VARIP,
706
+ type_hint=type_hint,
707
+ is_global=True
708
+ )
709
+ self.symbol_table.define(symbol)
710
+
711
+ # Register type declarations (UDTs)
712
+ for decl in script.declarations:
713
+ if isinstance(decl, TypeDecl):
714
+ symbol = Symbol(
715
+ decl.name,
716
+ VariableKind.TYPE,
717
+ is_global=True
718
+ )
719
+ self.symbol_table.define(symbol)
720
+
721
+ # Register function declarations
722
+ for decl in script.declarations:
723
+ if isinstance(decl, FuncDecl):
724
+ symbol = Symbol(
725
+ decl.name,
726
+ VariableKind.FUNCTION,
727
+ is_global=True
728
+ )
729
+ self.symbol_table.define(symbol)
730
+
731
+ # Register assignments as variables (including nested scopes)
732
+ self._register_assignments_recursive(script.body)
733
+
734
+ def _register_assignments_recursive(self, stmts: List[Statement]) -> None:
735
+ """Recursively register assignments in the symbol table, including nested scopes."""
736
+ for stmt in stmts:
737
+ if isinstance(stmt, Assignment) and isinstance(stmt.target, str):
738
+ if not self.symbol_table.lookup_global(stmt.target):
739
+ if hasattr(stmt, 'type_hint') and stmt.type_hint:
740
+ type_hint = convert_pine_generic_to_python(stmt.type_hint)
741
+ else:
742
+ type_hint = self.type_inference.infer_type_hint(stmt.value)
743
+ symbol = Symbol(
744
+ stmt.target,
745
+ VariableKind.SERIES,
746
+ type_hint=type_hint,
747
+ is_global=True
748
+ )
749
+ symbol.is_series = True
750
+ self.symbol_table.define(symbol)
751
+ # Recurse into nested scopes
752
+ if isinstance(stmt, IfStatement):
753
+ self._register_assignments_recursive(stmt.body)
754
+ for _, elif_body in stmt.elseifs:
755
+ self._register_assignments_recursive(elif_body)
756
+ if stmt.else_body:
757
+ self._register_assignments_recursive(stmt.else_body)
758
+ elif isinstance(stmt, (ForLoop, ForInLoop, WhileLoop)):
759
+ if isinstance(stmt.body, list):
760
+ self._register_assignments_recursive(stmt.body)
761
+
762
+ def _infer_type_from_declaration(self, decl: Union[VarDecl, VaripDecl]) -> str:
763
+ """Infer type from var/varip declaration."""
764
+ if decl.type_hint:
765
+ return convert_pine_generic_to_python(decl.type_hint)
766
+ return self.type_inference.infer_type_hint(decl.value)
767
+
768
+ # ========================================================================
769
+ # Module-level constant detection
770
+ # ========================================================================
771
+
772
+ def _collect_identifier_names(self, node: Any) -> set:
773
+ """Recursively collect all Identifier names referenced in an AST node."""
774
+ names = set()
775
+ if isinstance(node, Identifier):
776
+ names.add(node.name)
777
+ elif isinstance(node, (FunctionCall, MethodCall)):
778
+ for arg in getattr(node, 'args', []):
779
+ names.update(self._collect_identifier_names(arg))
780
+ for val in getattr(node, 'kwargs', {}).values():
781
+ names.update(self._collect_identifier_names(val))
782
+ elif isinstance(node, (BinaryOp,)):
783
+ names.update(self._collect_identifier_names(node.left))
784
+ names.update(self._collect_identifier_names(node.right))
785
+ elif isinstance(node, (UnaryOp,)):
786
+ names.update(self._collect_identifier_names(node.operand))
787
+ elif isinstance(node, (TernaryOp,)):
788
+ names.update(self._collect_identifier_names(node.condition))
789
+ names.update(self._collect_identifier_names(node.true_expr))
790
+ names.update(self._collect_identifier_names(node.false_expr))
791
+ elif isinstance(node, MemberAccess):
792
+ names.update(self._collect_identifier_names(node.object))
793
+ elif isinstance(node, IndexAccess):
794
+ names.update(self._collect_identifier_names(node.object))
795
+ names.update(self._collect_identifier_names(node.index))
796
+ return names
797
+
798
+ def _collect_module_constant_names(self, script: Script) -> set:
799
+ """Collect names of body assignments that must be module-level constants.
800
+
801
+ In Pine Script, literal assignments before the first function call
802
+ (e.g., `const string G_ENTRY = '...'`) can be referenced by input.*()
803
+ group= params and strategy/indicator kwargs. Since these become main()
804
+ parameter defaults (evaluated at definition time), the referenced
805
+ variables must exist at module level.
806
+ """
807
+ # Collect all identifier names referenced in input declarations
808
+ referenced_names = set()
809
+ for decl in script.declarations:
810
+ if isinstance(decl, InputDecl):
811
+ for arg in decl.args:
812
+ referenced_names.update(self._collect_identifier_names(arg))
813
+ for val in decl.kwargs.values():
814
+ referenced_names.update(self._collect_identifier_names(val))
815
+
816
+ # Also collect names referenced in strategy/indicator kwargs
817
+ if script.script_decl:
818
+ for val in script.script_decl.kwargs.values():
819
+ referenced_names.update(self._collect_identifier_names(val))
820
+
821
+ # Find body assignments that are literal values AND referenced by inputs/decorator
822
+ module_const_names = set()
823
+ for stmt in script.body:
824
+ if (isinstance(stmt, Assignment) and
825
+ isinstance(stmt.target, str) and
826
+ isinstance(stmt.value, Literal) and
827
+ stmt.target in referenced_names):
828
+ module_const_names.add(stmt.target)
829
+
830
+ # Also check var/varip declarations with literal values referenced by inputs/decorator
831
+ # (e.g., `var string G_STRATEGY = 'Strategy Settings'` used in group= params)
832
+ for decl in script.declarations:
833
+ if (isinstance(decl, (VarDecl, VaripDecl)) and
834
+ isinstance(decl.value, Literal) and
835
+ decl.name in referenced_names):
836
+ module_const_names.add(decl.name)
837
+
838
+ return module_const_names
839
+
840
+ # ========================================================================
841
+ # Top-level transformations
842
+ # ========================================================================
843
+
844
+ def _transform_script_declaration(self, decl: Union[IndicatorDecl, StrategyDecl]) -> Union[IndicatorDecl, StrategyDecl]:
845
+ """Transform indicator/strategy declaration (Rules 2-3, 17, 54)."""
846
+ # Transform kwargs: convert true/false to True/False (Rule 17, 54)
847
+ transformed_kwargs = {}
848
+ for key, value in decl.kwargs.items():
849
+ transformed_kwargs[key] = self._transform_decorator_value(value)
850
+
851
+ if isinstance(decl, IndicatorDecl):
852
+ return IndicatorDecl(title=decl.title, kwargs=transformed_kwargs)
853
+ else:
854
+ return StrategyDecl(title=decl.title, kwargs=transformed_kwargs)
855
+
856
+ def _transform_decorator_value(self, value: Any) -> Any:
857
+ """Transform decorator keyword argument values (Rule 17, 54)."""
858
+ if isinstance(value, Literal):
859
+ if value.literal_type == 'bool':
860
+ # Convert true/false to True/False (Rule 17)
861
+ return value.value # Will be True or False in Python
862
+ return value
863
+ elif isinstance(value, Identifier):
864
+ # Handle true/false identifiers
865
+ if value.name == 'true':
866
+ return True
867
+ elif value.name == 'false':
868
+ return False
869
+ return value
870
+
871
+ def _extract_input_declarations(self, script: Script) -> None:
872
+ """Extract input declarations for main() parameters (Rules 8-9)."""
873
+ for decl in script.declarations:
874
+ if isinstance(decl, InputDecl):
875
+ self.input_declarations.append(decl)
876
+
877
+ def _create_main_parameters(self) -> List[Parameter]:
878
+ """Create main() function parameters from input declarations (Rule 8)."""
879
+ params = []
880
+ self._source_input_wrappers = [] # Track input.source params needing Series wrappers
881
+
882
+ for input_decl in self.input_declarations:
883
+ args = list(input_decl.args)
884
+ kwargs = dict(input_decl.kwargs)
885
+
886
+ # Convert excess positional args to keyword args.
887
+ # Pine Script allows input.int(defval, title, minval, maxval, ...)
888
+ # but PyneCore requires minval=, maxval=, ... as keyword-only.
889
+ from .pine_builtins import INPUT_POSITIONAL_PARAMS
890
+ param_names = INPUT_POSITIONAL_PARAMS.get(input_decl.func)
891
+ if param_names and len(args) > 2:
892
+ excess_args = args[2:]
893
+ args = args[:2]
894
+ for i, val in enumerate(excess_args):
895
+ if i < len(param_names):
896
+ kwargs[param_names[i]] = val
897
+
898
+ func_call = FunctionCall(
899
+ func=input_decl.func,
900
+ args=args,
901
+ kwargs=kwargs
902
+ )
903
+
904
+ # input.source returns a float per bar — needs Series wrapping
905
+ # for [n] subscript access to work
906
+ if input_decl.func in ('input.source', 'input'):
907
+ # Check if any arg is a source identifier (close, open, high, low, etc.)
908
+ is_source = input_decl.func == 'input.source'
909
+ if not is_source and args:
910
+ # input(close, ...) where first arg is a source
911
+ first_arg = args[0]
912
+ if isinstance(first_arg, Identifier) and first_arg.name in (
913
+ 'close', 'open', 'high', 'low', 'volume', 'hl2', 'hlc3', 'ohlc4', 'hlcc4'
914
+ ):
915
+ is_source = True
916
+
917
+ if is_source:
918
+ # Rename param and add Series wrapper assignment to body
919
+ src_param_name = f'_{input_decl.name}_src'
920
+ param = Parameter(name=src_param_name, type_hint=None, default=func_call)
921
+ self._source_input_wrappers.append((input_decl.name, src_param_name))
922
+ params.append(param)
923
+ continue
924
+
925
+ param = Parameter(
926
+ name=input_decl.name,
927
+ type_hint=None,
928
+ default=func_call
929
+ )
930
+ params.append(param)
931
+
932
+ return params
933
+
934
+ # ========================================================================
935
+ # Function transformations
936
+ # ========================================================================
937
+
938
+ def _find_indexed_parameters(self, body: Union[Expression, List[Statement]], param_names: set) -> set:
939
+ """Find which parameters are accessed with indexing (e.g., source[i])."""
940
+ indexed_params = set()
941
+
942
+ def scan_expr(expr):
943
+ """Recursively scan expression for IndexAccess nodes."""
944
+ if expr is None:
945
+ return
946
+
947
+ if isinstance(expr, IndexAccess):
948
+ # Check if the indexed object is a parameter
949
+ if isinstance(expr.object, Identifier) and expr.object.name in param_names:
950
+ indexed_params.add(expr.object.name)
951
+ # Recursively scan the index expression
952
+ scan_expr(expr.index)
953
+ elif isinstance(expr, BinaryOp):
954
+ scan_expr(expr.left)
955
+ scan_expr(expr.right)
956
+ elif isinstance(expr, UnaryOp):
957
+ scan_expr(expr.operand)
958
+ elif isinstance(expr, FunctionCall):
959
+ for arg in expr.args:
960
+ scan_expr(arg)
961
+ elif isinstance(expr, MemberAccess):
962
+ scan_expr(expr.object)
963
+ elif isinstance(expr, TernaryOp):
964
+ scan_expr(expr.condition)
965
+ scan_expr(expr.true_expr)
966
+ scan_expr(expr.false_expr)
967
+
968
+ def scan_stmt(stmt):
969
+ """Recursively scan statement for IndexAccess nodes."""
970
+ if stmt is None:
971
+ return
972
+
973
+ if isinstance(stmt, Assignment):
974
+ scan_expr(stmt.value)
975
+ elif isinstance(stmt, Reassignment):
976
+ scan_expr(stmt.value)
977
+ elif isinstance(stmt, ExpressionStatement):
978
+ scan_expr(stmt.expr)
979
+ elif isinstance(stmt, ReturnStatement):
980
+ scan_expr(stmt.expr)
981
+ elif isinstance(stmt, IfStatement):
982
+ scan_expr(stmt.condition)
983
+ for s in stmt.body:
984
+ scan_stmt(s)
985
+ for cond, elseif_body in stmt.elseifs:
986
+ scan_expr(cond)
987
+ for s in elseif_body:
988
+ scan_stmt(s)
989
+ if stmt.else_body:
990
+ for s in stmt.else_body:
991
+ scan_stmt(s)
992
+ elif isinstance(stmt, ForLoop):
993
+ scan_expr(stmt.from_val)
994
+ scan_expr(stmt.to_val)
995
+ if stmt.step:
996
+ scan_expr(stmt.step)
997
+ for s in (stmt.body if isinstance(stmt.body, list) else [stmt.body]):
998
+ scan_stmt(s)
999
+ elif isinstance(stmt, ForInLoop):
1000
+ scan_expr(stmt.iterable)
1001
+ for s in (stmt.body if isinstance(stmt.body, list) else [stmt.body]):
1002
+ scan_stmt(s)
1003
+ elif isinstance(stmt, WhileLoop):
1004
+ scan_expr(stmt.condition)
1005
+ for s in (stmt.body if isinstance(stmt.body, list) else [stmt.body]):
1006
+ scan_stmt(s)
1007
+
1008
+ # Scan the body
1009
+ if isinstance(body, Expression):
1010
+ scan_expr(body)
1011
+ elif isinstance(body, list):
1012
+ for stmt in body:
1013
+ scan_stmt(stmt)
1014
+ else:
1015
+ scan_stmt(body)
1016
+
1017
+ return indexed_params
1018
+
1019
+ def _transform_function(self, func: FuncDecl) -> FuncDecl:
1020
+ """Transform function declaration (Rules 20-22, 46-48)."""
1021
+ # Save current function params context
1022
+ old_params = self.current_function_params
1023
+ self.current_function_params = {}
1024
+
1025
+ # PRIORITY 1 FIX: Save and set function context
1026
+ old_in_function = self.in_user_function
1027
+ old_series_vars = self.series_variables
1028
+ old_local_vars = self.function_local_vars
1029
+
1030
+ self.in_user_function = True
1031
+ self.series_variables = self._find_series_variables_in_function(
1032
+ func.body if isinstance(func.body, list) else [func.body]
1033
+ )
1034
+ self.function_local_vars = set()
1035
+
1036
+ # Find which parameters are indexed in the function body
1037
+ param_names = {p.name for p in func.params}
1038
+ indexed_params = self._find_indexed_parameters(func.body, param_names)
1039
+
1040
+ # BUG FIX: Detect parameter shadowing - when parameter name matches a module name
1041
+ # that is used in the default value. This is a Python limitation where default values
1042
+ # are evaluated at function definition time, creating ambiguity.
1043
+ # Solution: Generate aliased imports for shadowed modules (e.g., position as _position_module)
1044
+ for param in func.params:
1045
+ if param.default and param.name in KNOWN_MODULES:
1046
+ # Parameter name matches a known module - check if default value references that module
1047
+ module_refs = self._get_module_references_in_expr(param.default)
1048
+ if param.name in module_refs:
1049
+ # Shadowing detected! Generate alias for this module
1050
+ alias_name = f"_{param.name}_module"
1051
+ self.import_resolver.aliased_modules[param.name] = alias_name
1052
+ # Also ensure the module is in lib_modules so it gets imported
1053
+ self.import_resolver.lib_modules.add(param.name)
1054
+
1055
+ # Transform parameters
1056
+ # For methods, the first parameter has special syntax
1057
+ transformed_params = []
1058
+ for i, param in enumerate(func.params):
1059
+ # Convert Pine generic types to Python types
1060
+ if param.type_hint:
1061
+ type_hint = convert_pine_generic_to_python(param.type_hint)
1062
+ # Wrap in Series[T] if parameter is indexed with [n] history access.
1063
+ # The runtime SeriesTransformer will create SeriesImpl and register it.
1064
+ if param.name in indexed_params:
1065
+ type_hint = f'Series[{type_hint}]'
1066
+ else:
1067
+ # If parameter is indexed and has no type hint, add bare Series
1068
+ type_hint = 'Series' if param.name in indexed_params else None
1069
+
1070
+ # Track parameter types for method call transformation
1071
+ if type_hint:
1072
+ self.current_function_params[param.name] = type_hint
1073
+
1074
+ # Rewrite default value to use module aliases if needed
1075
+ default_value = None
1076
+ if param.default:
1077
+ # Check if this default value needs module alias rewriting
1078
+ if param.name in self.import_resolver.aliased_modules:
1079
+ default_value = self._rewrite_module_references(param.default)
1080
+ else:
1081
+ default_value = param.default
1082
+ # Now transform the (possibly rewritten) default value
1083
+ default_value = self._transform_expression(default_value)
1084
+
1085
+ transformed_params.append(Parameter(
1086
+ name=param.name,
1087
+ type_hint=type_hint,
1088
+ default=default_value
1089
+ ))
1090
+
1091
+ # Transform body
1092
+ if isinstance(func.body, Expression):
1093
+ # Single-line arrow function (Rule 21)
1094
+ transformed_body = ReturnStatement(expr=self._transform_expression(func.body))
1095
+ else:
1096
+ # Multi-line function (Rule 22)
1097
+ transformed_body = [self._transform_statement(stmt) for stmt in func.body]
1098
+ # PRIORITY 1 FIX: Flatten nested lists (from Series variable initialization)
1099
+ # and remove None entries
1100
+ flattened = []
1101
+ for s in transformed_body:
1102
+ if s is None:
1103
+ continue
1104
+ elif isinstance(s, list):
1105
+ flattened.extend(s)
1106
+ else:
1107
+ flattened.append(s)
1108
+ transformed_body = flattened
1109
+
1110
+ if transformed_body:
1111
+ last = transformed_body[-1]
1112
+ if isinstance(last, ExpressionStatement):
1113
+ # Add explicit return for last expression (Rule 20)
1114
+ transformed_body[-1] = ReturnStatement(expr=last.expr)
1115
+ elif isinstance(last, Assignment):
1116
+ # Last statement is an assignment - Pine Script returns the assigned value
1117
+ # Both `=` and `:=` assignments should return the target
1118
+ target_name = last.target if isinstance(last.target, str) else last.target.name
1119
+ transformed_body.append(ReturnStatement(
1120
+ expr=Identifier(name=target_name)
1121
+ ))
1122
+ elif isinstance(last, IfStatement):
1123
+ # If function ends with if/else block, apply __block_result__ pattern
1124
+ transformed_body = self._apply_block_result_pattern(transformed_body)
1125
+
1126
+ # Restore previous params context
1127
+ self.current_function_params = old_params
1128
+
1129
+ # PRIORITY 1 FIX: Restore function context
1130
+ self.in_user_function = old_in_function
1131
+ self.series_variables = old_series_vars
1132
+ self.function_local_vars = old_local_vars
1133
+
1134
+ # Track method functions for call transformation
1135
+ if func.is_method:
1136
+ self.method_functions.add(func.name)
1137
+
1138
+ # Sanitize function name if it clashes with Python reserved words
1139
+ from .pine_builtins import sanitize_identifier
1140
+ sanitized_name = sanitize_identifier(func.name)
1141
+ if sanitized_name != func.name:
1142
+ self._renamed_functions[func.name] = sanitized_name
1143
+
1144
+ return FuncDecl(
1145
+ name=sanitized_name,
1146
+ params=transformed_params,
1147
+ body=transformed_body,
1148
+ is_method=func.is_method,
1149
+ is_export=False, # Remove export keyword (Rule 46)
1150
+ indexed_params=indexed_params,
1151
+ )
1152
+
1153
+ def _resolve_overloaded_functions(self, main_body: List[Statement]) -> None:
1154
+ """Resolve function overloading by renaming overloads and generating dispatchers.
1155
+
1156
+ Pine Script supports function overloading (same name, different param types/counts).
1157
+ Python does not, so we rename each overload and generate a dispatcher function
1158
+ that checks argument count and types at runtime.
1159
+ """
1160
+ # Collect function declarations by name
1161
+ func_groups: Dict[str, List[tuple]] = {} # name -> [(index, FuncDecl)]
1162
+ for i, stmt in enumerate(main_body):
1163
+ if isinstance(stmt, FuncDecl):
1164
+ if stmt.name not in func_groups:
1165
+ func_groups[stmt.name] = []
1166
+ func_groups[stmt.name].append((i, stmt))
1167
+
1168
+ # Only process groups with multiple overloads
1169
+ overloaded = {name: group for name, group in func_groups.items() if len(group) > 1}
1170
+ if not overloaded:
1171
+ return
1172
+
1173
+ # Process each overloaded group
1174
+ # Work backwards to preserve indices when inserting
1175
+ for name, group in sorted(overloaded.items(), key=lambda x: x[1][-1][0], reverse=True):
1176
+ # Rename each overload
1177
+ for j, (idx, func) in enumerate(group, 1):
1178
+ func.name = f'_{name}_{j}'
1179
+
1180
+ # Generate dispatcher and insert after last overload
1181
+ dispatcher = self._generate_overload_dispatcher(name, [func for _, func in group])
1182
+ last_idx = group[-1][0]
1183
+ dispatcher.line = main_body[last_idx].line
1184
+ main_body.insert(last_idx + 1, dispatcher)
1185
+
1186
+ def _generate_overload_dispatcher(self, name: str, overloads: List[FuncDecl]) -> RawCode:
1187
+ """Generate a dispatcher function for overloaded functions.
1188
+
1189
+ Dispatches by argument count first, then by type of the first argument.
1190
+ """
1191
+ # Group overloads by parameter count
1192
+ by_count: Dict[int, List[FuncDecl]] = {}
1193
+ for func in overloads:
1194
+ count = len(func.params)
1195
+ if count not in by_count:
1196
+ by_count[count] = []
1197
+ by_count[count].append(func)
1198
+
1199
+ lines = [f'def {name}(*_args):']
1200
+
1201
+ # Generate dispatch logic
1202
+ counts = sorted(by_count.keys())
1203
+ for ci, count in enumerate(counts):
1204
+ funcs = by_count[count]
1205
+ prefix = 'if' if ci == 0 else 'elif'
1206
+ lines.append(f' {prefix} len(_args) == {count}:')
1207
+
1208
+ if len(funcs) == 1:
1209
+ # Single overload for this arg count — call directly
1210
+ args = ', '.join(f'_args[{i}]' for i in range(count))
1211
+ lines.append(f' return {funcs[0].name}({args})')
1212
+ else:
1213
+ # Multiple overloads — dispatch by type of first argument
1214
+ for fi, func in enumerate(funcs):
1215
+ type_hint = func.params[0].type_hint if func.params else None
1216
+ check = self._type_check_expr('_args[0]', type_hint)
1217
+ fi_prefix = 'if' if fi == 0 else 'elif'
1218
+ if fi == len(funcs) - 1:
1219
+ # Last overload is the fallback
1220
+ args = ', '.join(f'_args[{i}]' for i in range(count))
1221
+ lines.append(f' else:')
1222
+ lines.append(f' return {func.name}({args})')
1223
+ else:
1224
+ args = ', '.join(f'_args[{i}]' for i in range(count))
1225
+ lines.append(f' {fi_prefix} {check}:')
1226
+ lines.append(f' return {func.name}({args})')
1227
+
1228
+ return RawCode(code='\n'.join(lines))
1229
+
1230
+ @staticmethod
1231
+ def _type_check_expr(var: str, type_hint: Optional[str]) -> str:
1232
+ """Generate isinstance check expression for a Pine type hint."""
1233
+ if not type_hint:
1234
+ return 'True'
1235
+ # Map Pine/Python type hints to isinstance checks
1236
+ # bool must come before int/float since bool is a subclass of int
1237
+ type_map = {
1238
+ 'bool': f'isinstance({var}, bool)',
1239
+ 'int': f'isinstance({var}, int) and not isinstance({var}, bool)',
1240
+ 'float': f'isinstance({var}, (int, float)) and not isinstance({var}, bool)',
1241
+ 'str': f'isinstance({var}, str)',
1242
+ 'Color': f'not isinstance({var}, (bool, int, float, str))',
1243
+ }
1244
+ return type_map.get(type_hint, 'True')
1245
+
1246
+ def _apply_block_result_pattern(self, body: List[Statement]) -> List[Statement]:
1247
+ """Apply __block_result__ pattern when function ends with an if statement.
1248
+
1249
+ Only applies to the LAST statement in the body when it's an IfStatement.
1250
+ Inserts __block_result__ = na before it, converts last expression in each
1251
+ branch to __block_result__ = expr, and adds return __block_result__ after.
1252
+ """
1253
+ if not body:
1254
+ return body
1255
+
1256
+ last = body[-1]
1257
+ if not isinstance(last, IfStatement):
1258
+ return body
1259
+
1260
+ # Build new body: all statements before the last if, then the pattern
1261
+ new_body = list(body[:-1])
1262
+
1263
+ # Insert __block_result__ = na before the if
1264
+ new_body.append(Assignment(
1265
+ target='__block_result__',
1266
+ value=Identifier(name='na'),
1267
+ type_hint=None
1268
+ ))
1269
+ # Ensure na is imported (this assignment is created after import analysis)
1270
+ self.import_resolver.lib_modules.add('na')
1271
+
1272
+ # Transform the last if statement for __block_result__
1273
+ new_body.append(self._transform_if_for_block_result(last))
1274
+
1275
+ # Add return __block_result__
1276
+ new_body.append(ReturnStatement(expr=Identifier(name='__block_result__')))
1277
+
1278
+ return new_body
1279
+
1280
+ def _transform_if_for_block_result(self, if_stmt: IfStatement) -> IfStatement:
1281
+ """Transform if statement to assign last expression to __block_result__."""
1282
+ # Transform if body
1283
+ new_if_body = list(if_stmt.body)
1284
+ if new_if_body:
1285
+ last_stmt = new_if_body[-1]
1286
+
1287
+ # Determine what to assign to __block_result__
1288
+ if isinstance(last_stmt, Assignment):
1289
+ # After assignment, capture the assigned value
1290
+ # e.g., this.max_win = math.max(...) -> also assign this.max_win to __block_result__
1291
+ new_if_body.append(Assignment(
1292
+ target='__block_result__',
1293
+ value=Identifier(name=last_stmt.target) if isinstance(last_stmt.target, str) else last_stmt.target,
1294
+ type_hint=None
1295
+ ))
1296
+ elif isinstance(last_stmt, ExpressionStatement):
1297
+ # Replace expression statement with __block_result__ assignment
1298
+ new_if_body[-1] = Assignment(
1299
+ target='__block_result__',
1300
+ value=last_stmt.expr,
1301
+ type_hint=None
1302
+ )
1303
+ elif isinstance(last_stmt, ReturnStatement):
1304
+ # Replace return with __block_result__ assignment
1305
+ new_if_body[-1] = Assignment(
1306
+ target='__block_result__',
1307
+ value=last_stmt.expr,
1308
+ type_hint=None
1309
+ )
1310
+
1311
+ # Transform else body
1312
+ new_else_body = None
1313
+ if if_stmt.else_body:
1314
+ new_else_body = list(if_stmt.else_body)
1315
+ if new_else_body:
1316
+ last_stmt = new_else_body[-1]
1317
+
1318
+ # Determine what to assign to __block_result__
1319
+ if isinstance(last_stmt, Assignment):
1320
+ # After assignment, capture the assigned value
1321
+ new_else_body.append(Assignment(
1322
+ target='__block_result__',
1323
+ value=Identifier(name=last_stmt.target) if isinstance(last_stmt.target, str) else last_stmt.target,
1324
+ type_hint=None
1325
+ ))
1326
+ elif isinstance(last_stmt, ExpressionStatement):
1327
+ # Replace expression statement with __block_result__ assignment
1328
+ new_else_body[-1] = Assignment(
1329
+ target='__block_result__',
1330
+ value=last_stmt.expr,
1331
+ type_hint=None
1332
+ )
1333
+ elif isinstance(last_stmt, ReturnStatement):
1334
+ # Replace return with __block_result__ assignment
1335
+ new_else_body[-1] = Assignment(
1336
+ target='__block_result__',
1337
+ value=last_stmt.expr,
1338
+ type_hint=None
1339
+ )
1340
+
1341
+ return IfStatement(
1342
+ condition=if_stmt.condition,
1343
+ body=new_if_body,
1344
+ else_body=new_else_body
1345
+ )
1346
+
1347
+ # ========================================================================
1348
+ # Statement transformations
1349
+ # ========================================================================
1350
+
1351
+ def _transform_statement(self, stmt: Statement) -> Union[Statement, List[Statement], None]:
1352
+ """Transform statement."""
1353
+ # Clear hoisted statements before transforming
1354
+ self._hoisted_stmts = []
1355
+
1356
+ if isinstance(stmt, Assignment):
1357
+ result = self._transform_assignment(stmt)
1358
+ elif isinstance(stmt, Reassignment):
1359
+ result = self._transform_reassignment(stmt)
1360
+ elif isinstance(stmt, (VarDecl, VaripDecl)):
1361
+ result = self._transform_var_declaration(stmt)
1362
+ elif isinstance(stmt, IfStatement):
1363
+ result = self._transform_if_statement(stmt)
1364
+ elif isinstance(stmt, ForLoop):
1365
+ result = self._transform_for_loop(stmt)
1366
+ elif isinstance(stmt, ForInLoop):
1367
+ result = self._transform_for_in_loop(stmt)
1368
+ elif isinstance(stmt, WhileLoop):
1369
+ result = self._transform_while_loop(stmt)
1370
+ elif isinstance(stmt, SwitchStatement):
1371
+ result = self._transform_switch_statement(stmt)
1372
+ elif isinstance(stmt, ExpressionStatement):
1373
+ # Skip stray declaration calls (indicator/strategy/library) in body
1374
+ if isinstance(stmt.expr, FunctionCall) and isinstance(stmt.expr.func, str):
1375
+ if stmt.expr.func in ('indicator', 'strategy', 'library'):
1376
+ return None
1377
+ result = ExpressionStatement(expr=self._transform_expression(stmt.expr))
1378
+ elif isinstance(stmt, (BreakStatement, ContinueStatement)):
1379
+ result = stmt
1380
+ else:
1381
+ result = stmt
1382
+
1383
+ # Prepend any hoisted statements (e.g. from FunctionCall[N] extraction)
1384
+ hoisted = self._hoisted_stmts
1385
+ self._hoisted_stmts = []
1386
+ if hoisted:
1387
+ if isinstance(result, list):
1388
+ return hoisted + result
1389
+ elif result is not None:
1390
+ return hoisted + [result]
1391
+ return hoisted
1392
+ return result
1393
+
1394
+ def _transform_assignment(self, stmt: Assignment) -> Union[Assignment, List[Statement]]:
1395
+ """Transform assignment (Rules 5-7)."""
1396
+ # Handle for/for-in loop used as value expression:
1397
+ # x = for i in arr ... body → for i in arr: body; x = last_expr
1398
+ if isinstance(stmt.value, (ForInLoop, ForLoop)):
1399
+ return self._transform_for_as_expression(stmt)
1400
+
1401
+ # Handle TupleDestructure where any target needs Series annotation.
1402
+ # Split: [a, b] = func() → _tmp = func(); a: Series[float] = _tmp[0]; b = _tmp[1]
1403
+ if isinstance(stmt.target, TupleDestructure):
1404
+ indexed_names = self.type_inference.indexed_var_names
1405
+ needs_split = any(n in indexed_names for n in stmt.target.names)
1406
+ if needs_split:
1407
+ transformed_value = self._transform_expression(stmt.value)
1408
+ temp_name = '_tuple_tmp'
1409
+ statements = [Assignment(target=temp_name, value=transformed_value)]
1410
+ for index, name in enumerate(stmt.target.names):
1411
+ index_expr = IndexAccess(
1412
+ object=Identifier(name=temp_name),
1413
+ index=Literal(value=index, literal_type='int'),
1414
+ )
1415
+ if name in indexed_names:
1416
+ sym = self.symbol_table.lookup(name)
1417
+ base_type = (sym.type_hint if sym and sym.type_hint else
1418
+ self.type_inference.infer_type_hint(stmt.value))
1419
+ if base_type in ('Any', None):
1420
+ base_type = 'float'
1421
+ series_type_hint = f"Series[{base_type}]"
1422
+ self.import_resolver.uses_series = True
1423
+ else:
1424
+ series_type_hint = None
1425
+ statements.append(Assignment(target=name, value=index_expr, type_hint=series_type_hint))
1426
+ return statements
1427
+
1428
+ # PRIORITY 1 FIX: Track first assignment to Series variables in functions
1429
+ is_first_assignment_to_series = False
1430
+ if self.in_user_function and isinstance(stmt.target, str):
1431
+ if stmt.target in self.series_variables and stmt.target not in self.function_local_vars:
1432
+ is_first_assignment_to_series = True
1433
+ self.function_local_vars.add(stmt.target)
1434
+
1435
+ # Determine type annotation first (we need it for na value transformation)
1436
+ type_hint = None
1437
+ converted_type = None
1438
+ if isinstance(stmt.target, str):
1439
+ symbol = self.symbol_table.lookup(stmt.target)
1440
+ # CRITICAL: In Pine Script, series is the default type qualifier
1441
+ if hasattr(stmt, 'type_hint') and stmt.type_hint:
1442
+ # Use the explicit type hint from parser (e.g., map<int, float> mapExample = na)
1443
+ converted_type = convert_pine_generic_to_python(stmt.type_hint)
1444
+ # If variable is used with historical reference [], wrap in Series[type]
1445
+ if symbol and symbol.is_indexed:
1446
+ type_hint = f"Series[{converted_type}]"
1447
+ self.import_resolver.uses_series = True
1448
+ else:
1449
+ type_hint = converted_type
1450
+ elif symbol:
1451
+ # No explicit type in Pine source — only infer for na value transformation
1452
+ # and var/varip Persistent wrapping
1453
+ if symbol.kind in (VariableKind.VAR, VariableKind.VARIP):
1454
+ base_type = symbol.type_hint or self.type_inference.infer_type_hint(stmt.value)
1455
+ converted_type = base_type
1456
+ type_hint = f"Persistent[{base_type}]"
1457
+ elif isinstance(stmt.value, NaLiteral):
1458
+ # Need type for na() conversion even without explicit annotation
1459
+ base_type = symbol.type_hint or self.type_inference.infer_type_hint(stmt.value)
1460
+ converted_type = base_type
1461
+ elif isinstance(stmt.target, str) and stmt.target in getattr(self, '_reassignment_vars', set()) and symbol.is_indexed:
1462
+ # Variable uses both := (reassignment) and [] (historical reference)
1463
+ # WITHOUT 'var' keyword - this is a Series in Pine Script semantics
1464
+ base_type = symbol.type_hint or self.type_inference.infer_type_hint(stmt.value)
1465
+ if base_type == 'Any':
1466
+ base_type = 'float'
1467
+ converted_type = base_type
1468
+ type_hint = f"Series[{base_type}]"
1469
+ self.import_resolver.uses_series = True
1470
+ elif isinstance(stmt.target, str) and symbol.is_indexed:
1471
+ # Variable is used with [] - in Pine Script, series is the default type qualifier
1472
+ base_type = symbol.type_hint or self.type_inference.infer_type_hint(stmt.value)
1473
+ if base_type == 'Any':
1474
+ base_type = 'float'
1475
+ converted_type = base_type
1476
+ type_hint = f"Series[{base_type}]"
1477
+ self.import_resolver.uses_series = True
1478
+ # Otherwise: no type annotation for body assignments without explicit Pine types
1479
+
1480
+ # Transform value - special case for na with type hint
1481
+ if isinstance(stmt.value, NaLiteral) and converted_type:
1482
+ # Convert na to na(Type) where Type is the declared type
1483
+ value = FunctionCall(func='na', args=[Identifier(name=converted_type)], kwargs={})
1484
+ else:
1485
+ value = self._transform_expression(stmt.value)
1486
+
1487
+ # Suppress 'Any' type annotations - omit annotation when type is unknown
1488
+ if type_hint == 'Any':
1489
+ type_hint = None
1490
+
1491
+ # PRIORITY 1 FIX: For Series variables in functions, add Series type hint
1492
+ if is_first_assignment_to_series:
1493
+ self.import_resolver.uses_series = True
1494
+ # Get the base type for the Series (default to float — Pine's default series type)
1495
+ base_type = converted_type or self.type_inference.infer_type_hint(stmt.value)
1496
+ if base_type == 'Any':
1497
+ base_type = 'float'
1498
+ type_hint = f"Series[{base_type}]"
1499
+
1500
+ # Record this local's type in the current-function scope map so later
1501
+ # method calls on it resolve type-aware, e.g. `thisT = m.transpose()`
1502
+ # (thisT: Matrix[int]) then `thisT.sort(...)` → matrix.sort(...). Function
1503
+ # locals are not in the global symbol table, so this is the only place
1504
+ # their type is known; the map is per-function (saved/reset around each
1505
+ # function), and body statements transform in source order so the type is
1506
+ # in place before the call. Only concrete types are recorded.
1507
+ if self.in_user_function and isinstance(stmt.target, str):
1508
+ # Prefer the concrete converted type; otherwise infer from the
1509
+ # TRANSFORMED value (e.g. m.transpose() is now matrix.transpose(m),
1510
+ # whose return type the inference knows), not the pre-transform AST.
1511
+ local_type = converted_type or self.type_inference.infer_type_hint(value)
1512
+ if local_type and local_type != 'Any':
1513
+ self.current_function_params[stmt.target] = local_type
1514
+
1515
+ return Assignment(target=stmt.target, value=value, type_hint=type_hint)
1516
+
1517
+ def _transform_reassignment(self, stmt: Reassignment) -> Assignment:
1518
+ """
1519
+ Transform reassignment := to = (Rule 11).
1520
+ """
1521
+ # Default behavior: regular assignment
1522
+ # PRIORITY 1 FIX: Series type hint will be added during assignment transformation
1523
+ return Assignment(
1524
+ target=stmt.target,
1525
+ value=self._transform_expression(stmt.value)
1526
+ )
1527
+
1528
+ def _transform_var_declaration(self, decl: Union[VarDecl, VaripDecl]) -> Assignment:
1529
+ """Transform var/varip declaration to Persistent or Series assignment (Rule 5-6).
1530
+
1531
+ Scalar types (int, float, bool, str) use Series[T] = nz(name[1], initial)
1532
+ to support both persistence and [n] subscripting after reassignment.
1533
+ Complex types (dict, list, objects) use Persistent[T] since they're
1534
+ modified in-place and don't need reassignment.
1535
+ """
1536
+ base_type = decl.type_hint or self.type_inference.infer_type_hint(decl.value)
1537
+ # Convert generic types properly
1538
+ converted_type = convert_pine_generic_to_python(base_type)
1539
+
1540
+ # Scalar types that support the Series + nz pattern for var emulation
1541
+ SCALAR_TYPES = {'int', 'float', 'bool', 'str'}
1542
+
1543
+ # Custom UDT detection: uppercase name, not a built-in type, no generic brackets
1544
+ is_custom_udt = (
1545
+ converted_type and
1546
+ converted_type[0].isupper() and
1547
+ not converted_type.startswith(('Color', 'Line', 'Label', 'Box', 'Table', 'Matrix', 'Polyline', 'LineFill')) and
1548
+ '[' not in converted_type
1549
+ )
1550
+
1551
+ # Special case: if value is na, convert to na(Type) function call
1552
+ value = decl.value
1553
+ if isinstance(value, NaLiteral):
1554
+ value = FunctionCall(func='na', args=[Identifier(name=converted_type)], kwargs={})
1555
+ else:
1556
+ value = self._transform_expression(value)
1557
+
1558
+ # Scalar var declarations: use PersistentSeries[T] = initial
1559
+ # PersistentSeriesTransformer splits this into Persistent[T] + Series[T],
1560
+ # avoiding the self-referencing [1] off-by-one bug in the old nz(name[1], initial) pattern
1561
+ if converted_type in SCALAR_TYPES:
1562
+ type_hint = f"PersistentSeries[{converted_type}]"
1563
+ self.import_resolver.uses_persistent_series = True
1564
+ elif converted_type == 'Any':
1565
+ type_hint = 'Persistent'
1566
+ elif is_custom_udt:
1567
+ # Custom UDTs need Persistent to survive across bars (var semantics)
1568
+ type_hint = f"Persistent[{converted_type}]"
1569
+ else:
1570
+ type_hint = f"Persistent[{converted_type}]"
1571
+
1572
+ return Assignment(
1573
+ target=decl.name,
1574
+ value=value,
1575
+ type_hint=type_hint
1576
+ )
1577
+
1578
+ def _transform_if_statement(self, stmt: IfStatement) -> IfStatement:
1579
+ """Transform if statement (Rule 15)."""
1580
+ condition = self._transform_expression(stmt.condition)
1581
+ # Save hoisted stmts from condition before body transforms clear them
1582
+ condition_hoisted = list(self._hoisted_stmts)
1583
+ self._hoisted_stmts = []
1584
+ body = [self._transform_statement(s) for s in stmt.body]
1585
+
1586
+ elseifs = []
1587
+ for elif_cond, elif_body in stmt.elseifs:
1588
+ transformed_cond = self._transform_expression(elif_cond)
1589
+ # Save hoisted stmts from elif condition
1590
+ elif_hoisted = list(self._hoisted_stmts)
1591
+ self._hoisted_stmts = []
1592
+ transformed_body = [self._transform_statement(s) for s in elif_body]
1593
+ condition_hoisted.extend(elif_hoisted)
1594
+ elseifs.append((transformed_cond, transformed_body))
1595
+
1596
+ else_body = None
1597
+ if stmt.else_body:
1598
+ else_body = [self._transform_statement(s) for s in stmt.else_body]
1599
+
1600
+ # Restore all condition hoisted stmts so they propagate to enclosing _transform_statement
1601
+ self._hoisted_stmts = condition_hoisted
1602
+ return IfStatement(
1603
+ condition=condition,
1604
+ body=body,
1605
+ elseifs=elseifs,
1606
+ else_body=else_body
1607
+ )
1608
+
1609
+ def _transform_for_loop(self, stmt: ForLoop) -> ForLoop:
1610
+ """Transform for loop to range() (Rules 12-13)."""
1611
+ from_val = self._transform_expression(stmt.from_val)
1612
+ to_val = self._transform_expression(stmt.to_val)
1613
+ step = self._transform_expression(stmt.step) if stmt.step else None
1614
+ # Save hoisted stmts from expressions before body transforms clear them
1615
+ expr_hoisted = list(self._hoisted_stmts)
1616
+ self._hoisted_stmts = []
1617
+
1618
+ body = [self._transform_statement(s) for s in stmt.body]
1619
+
1620
+ # Restore expression hoisted stmts
1621
+ self._hoisted_stmts = expr_hoisted
1622
+ return ForLoop(
1623
+ var=stmt.var,
1624
+ from_val=from_val,
1625
+ to_val=to_val,
1626
+ step=step,
1627
+ body=body
1628
+ )
1629
+
1630
+ def _transform_for_in_loop(self, stmt: ForInLoop) -> ForInLoop:
1631
+ """Transform for-in loop (Rule 14)."""
1632
+ iterable = self._transform_expression(stmt.iterable)
1633
+ # Save hoisted stmts from iterable before body transforms clear them
1634
+ expr_hoisted = list(self._hoisted_stmts)
1635
+ self._hoisted_stmts = []
1636
+ body = [self._transform_statement(s) for s in stmt.body]
1637
+
1638
+ # Restore expression hoisted stmts
1639
+ self._hoisted_stmts = expr_hoisted
1640
+ return ForInLoop(
1641
+ vars=stmt.vars,
1642
+ iterable=iterable,
1643
+ body=body
1644
+ )
1645
+
1646
+ def _transform_for_as_expression(self, stmt: Assignment) -> List[Statement]:
1647
+ """Transform for/for-in loop used as a value expression.
1648
+ x = for i in arr ... body → initialize x, then for loop with last expr assigning to x.
1649
+ """
1650
+ target = stmt.target if isinstance(stmt.target, str) else str(stmt.target)
1651
+ loop = stmt.value
1652
+
1653
+ # Initialize variable with empty string or na
1654
+ type_hint = None
1655
+ if hasattr(stmt, 'type_hint') and stmt.type_hint:
1656
+ type_hint = convert_pine_generic_to_python(stmt.type_hint)
1657
+ init_value = Literal(value='', literal_type='string') if type_hint == 'str' else NaLiteral()
1658
+ init_stmt = Assignment(target=target, value=init_value, type_hint=type_hint)
1659
+
1660
+ # Transform loop body — the last expression becomes an assignment to target
1661
+ body = [self._transform_statement(s) for s in loop.body if s is not None]
1662
+ if body:
1663
+ last = body[-1]
1664
+ if isinstance(last, ExpressionStatement):
1665
+ # Replace last ExpressionStatement with assignment to target
1666
+ body[-1] = Assignment(target=target, value=last.expr)
1667
+ elif isinstance(last, (Assignment, Reassignment)):
1668
+ # Add re-assignment of target after the last statement
1669
+ value = Identifier(name=last.target if isinstance(last.target, str) else str(last.target))
1670
+ body.append(Assignment(target=target, value=value))
1671
+
1672
+ # Build the transformed loop
1673
+ if isinstance(loop, ForInLoop):
1674
+ transformed_loop = ForInLoop(
1675
+ vars=loop.vars,
1676
+ iterable=self._transform_expression(loop.iterable),
1677
+ body=body
1678
+ )
1679
+ else:
1680
+ transformed_loop = ForLoop(
1681
+ var=loop.var,
1682
+ from_val=self._transform_expression(loop.from_val),
1683
+ to_val=self._transform_expression(loop.to_val),
1684
+ step=self._transform_expression(loop.step) if loop.step else None,
1685
+ body=body
1686
+ )
1687
+
1688
+ return [init_stmt, transformed_loop]
1689
+
1690
+ def _transform_while_loop(self, stmt: WhileLoop) -> WhileLoop:
1691
+ """Transform while loop."""
1692
+ condition = self._transform_expression(stmt.condition)
1693
+ # Save hoisted stmts from condition before body transforms clear them
1694
+ condition_hoisted = list(self._hoisted_stmts)
1695
+ self._hoisted_stmts = []
1696
+ body = [self._transform_statement(s) for s in stmt.body]
1697
+
1698
+ # Restore condition hoisted stmts
1699
+ self._hoisted_stmts = condition_hoisted
1700
+ return WhileLoop(condition=condition, body=body)
1701
+
1702
+ def _transform_switch_statement(self, stmt: SwitchStatement) -> IfStatement:
1703
+ """Transform switch to if/elif/else chain (Rules 41-42)."""
1704
+ # Convert switch cases to if/elif/else
1705
+ if not stmt.cases:
1706
+ return None
1707
+
1708
+ all_expr_hoisted = []
1709
+
1710
+ # First case becomes if
1711
+ first_case_expr, first_case_body = stmt.cases[0]
1712
+
1713
+ if stmt.expr:
1714
+ # switch expr => cases compare against expr
1715
+ first_condition = BinaryOp(
1716
+ left=self._transform_expression(stmt.expr),
1717
+ op='==',
1718
+ right=self._transform_expression(first_case_expr)
1719
+ )
1720
+ else:
1721
+ # switch without expr => cases are boolean conditions
1722
+ first_condition = self._transform_expression(first_case_expr)
1723
+
1724
+ # Save hoisted stmts from first condition
1725
+ all_expr_hoisted.extend(self._hoisted_stmts)
1726
+ self._hoisted_stmts = []
1727
+ transformed_body = [self._transform_statement(s) for s in first_case_body]
1728
+
1729
+ # Remaining cases become elif
1730
+ elseifs = []
1731
+ for case_expr, case_body in stmt.cases[1:]:
1732
+ if stmt.expr:
1733
+ elif_condition = BinaryOp(
1734
+ left=self._transform_expression(stmt.expr),
1735
+ op='==',
1736
+ right=self._transform_expression(case_expr)
1737
+ )
1738
+ else:
1739
+ elif_condition = self._transform_expression(case_expr)
1740
+
1741
+ # Save hoisted stmts from elif condition
1742
+ all_expr_hoisted.extend(self._hoisted_stmts)
1743
+ self._hoisted_stmts = []
1744
+ elif_body = [self._transform_statement(s) for s in case_body]
1745
+ elseifs.append((elif_condition, elif_body))
1746
+
1747
+ # Default case becomes else
1748
+ else_body = None
1749
+ if stmt.default:
1750
+ else_body = [self._transform_statement(s) for s in stmt.default]
1751
+
1752
+ # Restore all expression hoisted stmts
1753
+ self._hoisted_stmts = all_expr_hoisted
1754
+ return IfStatement(
1755
+ condition=first_condition,
1756
+ body=transformed_body,
1757
+ elseifs=elseifs,
1758
+ else_body=else_body
1759
+ )
1760
+
1761
+ # ========================================================================
1762
+ # Expression transformations
1763
+ # ========================================================================
1764
+
1765
+ def _transform_expression(self, expr: Expression) -> Expression:
1766
+ """Transform expression."""
1767
+ if isinstance(expr, BinaryOp):
1768
+ return BinaryOp(
1769
+ left=self._transform_expression(expr.left),
1770
+ op=expr.op,
1771
+ right=self._transform_expression(expr.right)
1772
+ )
1773
+ elif isinstance(expr, UnaryOp):
1774
+ return UnaryOp(
1775
+ op=expr.op,
1776
+ operand=self._transform_expression(expr.operand)
1777
+ )
1778
+ elif isinstance(expr, TernaryOp):
1779
+ return self._transform_ternary(expr)
1780
+ elif isinstance(expr, FunctionCall):
1781
+ return self._transform_function_call(expr)
1782
+ elif isinstance(expr, MethodCall):
1783
+ return self._transform_method_call(expr)
1784
+ elif isinstance(expr, MemberAccess):
1785
+ return self._transform_member_access(expr)
1786
+ elif isinstance(expr, IndexAccess):
1787
+ return self._transform_index_access(expr)
1788
+ elif isinstance(expr, Identifier):
1789
+ return self._transform_identifier(expr)
1790
+ elif isinstance(expr, Literal):
1791
+ return self._transform_literal(expr)
1792
+ elif isinstance(expr, NaLiteral):
1793
+ return self._transform_na_literal(expr)
1794
+ elif isinstance(expr, ArrayLiteral):
1795
+ return ArrayLiteral(elements=[self._transform_expression(e) for e in expr.elements])
1796
+ elif isinstance(expr, IfExpression):
1797
+ return self._transform_if_expression(expr)
1798
+ elif isinstance(expr, SwitchExpression):
1799
+ return self._transform_switch_expression(expr)
1800
+ else:
1801
+ return expr
1802
+
1803
+ def _transform_ternary(self, expr: TernaryOp) -> TernaryOp:
1804
+ """Transform ternary operator (Rule 10): cond ? a : b => a if cond else b"""
1805
+ # Note: We keep it as TernaryOp, codegen will convert to Python syntax
1806
+ return TernaryOp(
1807
+ condition=self._transform_expression(expr.condition),
1808
+ true_expr=self._transform_expression(expr.true_expr),
1809
+ false_expr=self._transform_expression(expr.false_expr)
1810
+ )
1811
+
1812
+ def _transform_generic_function_name(self, func_name: str) -> str:
1813
+ """Transform function name with generics."""
1814
+ if '<' not in func_name:
1815
+ return func_name
1816
+
1817
+ # For map.new<K,V>() -> map.new() (just strip generics)
1818
+ if 'map.new<' in func_name or func_name.startswith('map.new<'):
1819
+ return 'map.new'
1820
+
1821
+ # For array.new<T>() -> array.new_T()
1822
+ if 'array.new<' in func_name or func_name.startswith('array.new<'):
1823
+ # Extract the type from array.new<type>
1824
+ start = func_name.index('<') + 1
1825
+ end = func_name.index('>')
1826
+ type_param = func_name[start:end].strip()
1827
+
1828
+ # Map Pine Script types to PyneCore array.new_* functions
1829
+ type_map = {
1830
+ 'string': 'array.new_string',
1831
+ 'int': 'array.new_int',
1832
+ 'float': 'array.new_float',
1833
+ 'bool': 'array.new_bool',
1834
+ 'color': 'array.new_color',
1835
+ 'line': 'array.new_line',
1836
+ 'label': 'array.new_label',
1837
+ 'box': 'array.new_box',
1838
+ 'table': 'array.new_table',
1839
+ }
1840
+
1841
+ return type_map.get(type_param, 'list')
1842
+
1843
+ # For matrix.new<T>() -> matrix.new() (just strip generics)
1844
+ if 'matrix.new<' in func_name or func_name.startswith('matrix.new<'):
1845
+ return 'matrix.new'
1846
+
1847
+ # For other generic functions, strip generics for now
1848
+ base_name = func_name[:func_name.index('<')]
1849
+ return base_name
1850
+
1851
+ def _parse_dotted_path(self, path: str) -> Expression:
1852
+ """Parse a dotted path string like 'obj.lbl' into nested MemberAccess expressions."""
1853
+ parts = path.split('.')
1854
+ if len(parts) == 1:
1855
+ return Identifier(name=parts[0])
1856
+
1857
+ # Build nested MemberAccess from left to right
1858
+ expr = Identifier(name=parts[0])
1859
+ for part in parts[1:]:
1860
+ expr = MemberAccess(object=expr, member=part)
1861
+ return expr
1862
+
1863
+ def _resolve_module_for_method(self, method_name: str, type_str: str = None) -> Optional[str]:
1864
+ """Resolve PyneCore module name from method name and/or object type.
1865
+
1866
+ Two modes:
1867
+ - Type-aware (type_str provided): derive module from type string.
1868
+ - Heuristic (no type_str): infer module from method name using canonical sets.
1869
+ """
1870
+ if type_str:
1871
+ # Unwrap Persistent[X] or Series[X]
1872
+ if type_str.startswith('Persistent[') or type_str.startswith('Series['):
1873
+ start = type_str.index('[') + 1
1874
+ end = type_str.rindex(']')
1875
+ type_str = type_str[start:end]
1876
+
1877
+ if type_str.startswith('list'):
1878
+ return 'array'
1879
+ elif type_str.startswith('dict'):
1880
+ return 'map'
1881
+ elif type_str in ('line', 'label', 'box', 'table', 'linefill',
1882
+ 'Line', 'Label', 'Box', 'Table'):
1883
+ return type_str.lower()
1884
+ elif type_str.startswith('matrix') or type_str.startswith('Matrix'):
1885
+ return 'matrix'
1886
+ elif '<' in type_str:
1887
+ return type_str[:type_str.index('<')]
1888
+ return None
1889
+
1890
+ # Heuristic: infer module from method name alone.
1891
+ # Priority: drawing-specific > map-unique > array-unique > shared (map default)
1892
+ if method_name in LABEL_METHODS:
1893
+ return 'label'
1894
+ elif method_name in LINE_METHODS:
1895
+ return 'line'
1896
+ elif method_name in BOX_METHODS:
1897
+ return 'box'
1898
+ elif method_name in TABLE_METHODS:
1899
+ return 'table'
1900
+ elif method_name in MAP_UNIQUE_METHODS:
1901
+ return 'map'
1902
+ elif method_name in ARRAY_METHODS:
1903
+ return 'array'
1904
+ elif method_name in SHARED_COLLECTION_METHODS:
1905
+ # Shared methods (get, set, size, etc.) — default to array since
1906
+ # it's the most common collection, but map/matrix may also use these.
1907
+ # Type-aware path should be preferred when possible.
1908
+ return 'array'
1909
+ elif method_name in MATRIX_METHODS:
1910
+ return 'matrix'
1911
+ return None
1912
+
1913
+ def _transform_dotted_string_method_call(self, call: FunctionCall, func: str) -> Optional[FunctionCall]:
1914
+ """Handle var.method() → module.method(var, ...) for dotted string function names.
1915
+
1916
+ Returns transformed FunctionCall or None if not a method call pattern.
1917
+ """
1918
+ parts = func.split('.', 1) # Split only on first dot
1919
+ var_name = parts[0]
1920
+ method_name = parts[1] if len(parts) > 1 else None
1921
+
1922
+ if not method_name or var_name[0].isupper(): # Not a variable method call
1923
+ return None
1924
+
1925
+ # Check user-defined @method functions first
1926
+ if method_name in self.method_functions:
1927
+ transformed_args = [Identifier(name=var_name)] + [self._transform_expression(arg) for arg in call.args]
1928
+ transformed_kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
1929
+ return FunctionCall(func=method_name, args=transformed_args, kwargs=transformed_kwargs)
1930
+
1931
+ # Check if var_name has a known built-in type
1932
+ type_str = self.current_function_params.get(var_name)
1933
+ if not type_str:
1934
+ symbol = self.symbol_table.lookup(var_name)
1935
+ if symbol and symbol.type_hint:
1936
+ type_str = symbol.type_hint
1937
+
1938
+ if type_str:
1939
+ module_name = self._resolve_module_for_method(method_name, type_str)
1940
+ if module_name:
1941
+ self.import_resolver.add_module(module_name)
1942
+ transformed_args = [Identifier(name=var_name)] + [self._transform_expression(arg) for arg in call.args]
1943
+ transformed_kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
1944
+ return FunctionCall(func=f'{module_name}.{method_name}', args=transformed_args, kwargs=transformed_kwargs)
1945
+
1946
+ return None
1947
+
1948
+ def _transform_chained_dotted_method_call(self, call: FunctionCall, func: str) -> Optional[FunctionCall]:
1949
+ """Handle obj.field.method() → module.method(obj.field, ...) for chained access.
1950
+
1951
+ Returns transformed FunctionCall or None if not a chained method pattern.
1952
+ """
1953
+ first_component = func.split('.')[0]
1954
+ if first_component in KNOWN_MODULES or first_component[0].isupper():
1955
+ return None
1956
+
1957
+ parts = func.rsplit('.', 1) # Split on LAST dot
1958
+ obj_path = parts[0] # "obj.lbl"
1959
+ method_name = parts[1] # "set_x"
1960
+
1961
+ # Special case: .copy() on object fields -> udt_copy()
1962
+ # Must be checked BEFORE set matching (Location 1 behavior)
1963
+ if method_name == 'copy':
1964
+ obj_expr = self._parse_dotted_path(obj_path)
1965
+ transformed_args = [obj_expr] + [self._transform_expression(arg) for arg in call.args]
1966
+ transformed_kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
1967
+ self.import_resolver.uses_udt_copy = True
1968
+ return FunctionCall(func='udt_copy', args=transformed_args, kwargs=transformed_kwargs)
1969
+
1970
+ module_name = self._resolve_module_for_method(method_name)
1971
+ if module_name:
1972
+ self.import_resolver.add_module(module_name)
1973
+ obj_expr = self._parse_dotted_path(obj_path)
1974
+ transformed_args = [obj_expr] + [self._transform_expression(arg) for arg in call.args]
1975
+ transformed_kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
1976
+ return FunctionCall(func=f'{module_name}.{method_name}', args=transformed_args, kwargs=transformed_kwargs)
1977
+
1978
+ return None
1979
+
1980
+ def _transform_member_access_method_call(self, call: FunctionCall, func: MemberAccess) -> Optional[FunctionCall]:
1981
+ """Handle MemberAccess-based method calls like obj.lbl.set_x(...).
1982
+
1983
+ Returns transformed FunctionCall or None if not a recognized method.
1984
+ """
1985
+ method_name = func.member
1986
+ module_name = self._resolve_module_for_method(method_name)
1987
+
1988
+ if module_name:
1989
+ self.import_resolver.add_module(module_name)
1990
+ obj_expr = self._transform_expression(func.object)
1991
+ transformed_args = [obj_expr] + [self._transform_expression(arg) for arg in call.args]
1992
+ transformed_kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
1993
+ return FunctionCall(func=f'{module_name}.{method_name}', args=transformed_args, kwargs=transformed_kwargs)
1994
+
1995
+ return None
1996
+
1997
+ def _transform_function_call(self, call: FunctionCall) -> FunctionCall:
1998
+ """Transform function call (Rules 23-25, 49-51)."""
1999
+ func = call.func
2000
+ if isinstance(func, str):
2001
+ # Apply renamed function mapping (e.g., pass -> pass_)
2002
+ if func in self._renamed_functions:
2003
+ func = self._renamed_functions[func]
2004
+ call = FunctionCall(func=func, args=call.args, kwargs=call.kwargs)
2005
+
2006
+ # Convert excess positional args to keyword args for input.* calls.
2007
+ # Pine Script allows input.int(defval, title, minval, maxval, ...)
2008
+ # but PyneCore requires minval=, maxval=, ... as keyword-only.
2009
+ from .pine_builtins import INPUT_POSITIONAL_PARAMS
2010
+ param_names = INPUT_POSITIONAL_PARAMS.get(func)
2011
+ if param_names and len(call.args) > 2:
2012
+ new_args = list(call.args[:2])
2013
+ new_kwargs = dict(call.kwargs)
2014
+ for i, val in enumerate(call.args[2:]):
2015
+ if i < len(param_names):
2016
+ new_kwargs[param_names[i]] = val
2017
+ call = FunctionCall(func=func, args=new_args, kwargs=new_kwargs)
2018
+
2019
+ # Handle method calls disguised as dotted function calls
2020
+ if '.' in func and '<' not in func and '.' not in func.split('.')[0]:
2021
+ # Try var.method() → module.method(var, ...)
2022
+ result = self._transform_dotted_string_method_call(call, func)
2023
+ if result:
2024
+ return result
2025
+
2026
+ # Try chained member access: obj.lbl.set_x → label.set_x(obj.lbl, ...)
2027
+ if '.' in func:
2028
+ result = self._transform_chained_dotted_method_call(call, func)
2029
+ if result:
2030
+ return result
2031
+
2032
+ # SPECIAL CASE: timeframe.in_seconds() with no args
2033
+ if func == 'timeframe.in_seconds' and len(call.args) == 0 and not call.kwargs:
2034
+ return FunctionCall(
2035
+ func='timeframe.in_seconds',
2036
+ args=[MemberAccess(object=Identifier(name='timeframe'), member='period')],
2037
+ kwargs={},
2038
+ )
2039
+
2040
+ # SPECIAL CASE: array.max(a, nth) / array.min(a, nth) with 2 args
2041
+ if func in ('array.max', 'array.min') and len(call.args) == 2:
2042
+ self.import_resolver.add_module('order')
2043
+ arr_expr = self._transform_expression(call.args[0])
2044
+ nth_expr = self._transform_expression(call.args[1])
2045
+ sort_order = 'order.descending' if func == 'array.max' else 'order.ascending'
2046
+ inner = FunctionCall(
2047
+ func='array.sort_indices',
2048
+ args=[arr_expr, Identifier(name=sort_order)],
2049
+ kwargs={},
2050
+ )
2051
+ idx = FunctionCall(func='array.get', args=[inner, nth_expr], kwargs={})
2052
+ return FunctionCall(func='array.get', args=[arr_expr, idx], kwargs={})
2053
+
2054
+ # Handle generic syntax (map.new<K,V>, array.new<T>, etc.)
2055
+ if '<' in func:
2056
+ func = self._transform_generic_function_name(func)
2057
+ elif '.new' in func:
2058
+ base_name = func.replace('.new', '')
2059
+ if base_name and base_name[0].isupper():
2060
+ func = base_name
2061
+ elif func == 'int':
2062
+ func = 'cast_int'
2063
+ self.import_resolver.uses_cast_int = True
2064
+ elif func == 'float':
2065
+ func = 'cast_float'
2066
+ self.import_resolver.uses_cast_float = True
2067
+ elif func in ('color', 'bool', 'string'):
2068
+ # Type cast: color(na) → na, color(x) → x
2069
+ # When arg is na, return plain na (Pine na is untyped at runtime)
2070
+ if len(call.args) == 1 and isinstance(call.args[0], NaLiteral):
2071
+ self.import_resolver.add_module('na')
2072
+ return NaLiteral()
2073
+ # For non-na args, pass through as identity (type casts are no-ops in Python)
2074
+ if len(call.args) == 1:
2075
+ return self._transform_expression(call.args[0])
2076
+ func = func # keep as-is for module usage
2077
+ else:
2078
+ func = get_function_name(func)
2079
+ if '.' in func:
2080
+ parts = func.split('.')
2081
+ parts[0] = get_module_name(parts[0])
2082
+ func = '.'.join(parts)
2083
+
2084
+ elif isinstance(func, MemberAccess):
2085
+ result = self._transform_member_access_method_call(call, func)
2086
+ if result:
2087
+ return result
2088
+
2089
+ # Handle generics in member access
2090
+ if '<' in func.member:
2091
+ func.member = self._transform_generic_function_name(f"{func.object if isinstance(func.object, str) else 'obj'}.{func.member}")
2092
+ if func.member in ('dict', 'list'):
2093
+ func = func.member
2094
+ else:
2095
+ func = self._transform_member_access(func)
2096
+
2097
+ # Transform arguments
2098
+ transformed_args = [self._transform_expression(arg) for arg in call.args]
2099
+ transformed_kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
2100
+ return FunctionCall(func=func, args=transformed_args, kwargs=transformed_kwargs)
2101
+
2102
+ def _transform_method_call(self, call: MethodCall) -> Union[MethodCall, FunctionCall]:
2103
+ """Transform method call (Rule 48)."""
2104
+ # BUG FIX: Transform user-defined @method functions
2105
+ # Example: (expr).even_decimal() -> even_decimal(expr)
2106
+ # This must be first to handle methods on expressions, not just simple identifiers
2107
+ if call.method in self.method_functions:
2108
+ obj = self._transform_expression(call.object)
2109
+ args = [obj] + [self._transform_expression(arg) for arg in call.args]
2110
+ kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
2111
+ return FunctionCall(func=call.method, args=args, kwargs=kwargs)
2112
+
2113
+ # Special case: UDT.new() should become UDT() (remove .new())
2114
+ if call.method == 'new' and isinstance(call.object, Identifier):
2115
+ # Check if this is a UDT type (starts with uppercase)
2116
+ if call.object.name[0].isupper():
2117
+ # Convert to function call: Statistics.new() -> Statistics()
2118
+ args = [self._transform_expression(arg) for arg in call.args]
2119
+ kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
2120
+ return FunctionCall(func=call.object.name, args=args, kwargs=kwargs)
2121
+
2122
+ # Special case: .copy() on UDT object fields should become udt_copy()
2123
+ # Example: this.lbl.copy() -> udt_copy(this.lbl)
2124
+ if call.method == 'copy' and isinstance(call.object, MemberAccess):
2125
+ obj_expr = self._transform_expression(call.object)
2126
+ args = [obj_expr] + [self._transform_expression(arg) for arg in call.args]
2127
+ kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
2128
+ self.import_resolver.uses_udt_copy = True
2129
+ return FunctionCall(func='udt_copy', args=args, kwargs=kwargs)
2130
+
2131
+ # Transform method calls on built-in types to function calls
2132
+ # Example: id.push(value) -> array.push(id, value)
2133
+ type_str = None
2134
+ if isinstance(call.object, Identifier):
2135
+ # Look up the variable's type - check current function params first, then symbol table
2136
+ type_str = self.current_function_params.get(call.object.name)
2137
+ if not type_str:
2138
+ symbol = self.symbol_table.lookup(call.object.name)
2139
+ if symbol and symbol.type_hint:
2140
+ type_str = symbol.type_hint
2141
+ elif isinstance(call.object, MemberAccess):
2142
+ # For member access like this.lbl, infer type from method name heuristically
2143
+ module_from_method = self._resolve_module_for_method(call.method)
2144
+ if module_from_method:
2145
+ type_str = {
2146
+ 'label': 'Label', 'line': 'Line', 'box': 'Box',
2147
+ 'table': 'Table', 'matrix': 'Matrix',
2148
+ }.get(module_from_method)
2149
+
2150
+ # Resolve module: type-aware first, then heuristic fallback
2151
+ module_name = None
2152
+ if type_str:
2153
+ module_name = self._resolve_module_for_method(call.method, type_str)
2154
+ if not module_name:
2155
+ # Heuristic fallback when type is unknown
2156
+ module_name = self._resolve_module_for_method(call.method)
2157
+
2158
+ if module_name and module_name in ('array', 'line', 'label', 'box', 'table', 'map', 'matrix', 'linefill'):
2159
+ # Ensure the resolved module is imported
2160
+ self.import_resolver.add_module(module_name)
2161
+ obj = self._transform_expression(call.object)
2162
+ args = [obj] + [self._transform_expression(arg) for arg in call.args]
2163
+ kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
2164
+ func_name = f'{module_name}.{call.method}'
2165
+ return FunctionCall(func=func_name, args=args, kwargs=kwargs)
2166
+
2167
+ obj = self._transform_expression(call.object)
2168
+ args = [self._transform_expression(arg) for arg in call.args]
2169
+ kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
2170
+
2171
+ return MethodCall(object=obj, method=call.method, args=args, kwargs=kwargs)
2172
+
2173
+ def _transform_member_access(self, expr: MemberAccess) -> MemberAccess:
2174
+ """Transform member access (Rules 23-25)."""
2175
+ # Handle module renames
2176
+ if isinstance(expr.object, str):
2177
+ obj = get_module_name(expr.object)
2178
+ else:
2179
+ obj = self._transform_expression(expr.object)
2180
+
2181
+ # Handle plot style remapping (Rule 25)
2182
+ if isinstance(expr.object, str):
2183
+ full_name = f"{expr.object}.{expr.member}"
2184
+ if needs_plot_style_remap(full_name):
2185
+ remapped = get_plot_style_remap(full_name)
2186
+ parts = remapped.split('.')
2187
+ return MemberAccess(object=parts[0], member=parts[1])
2188
+
2189
+ return MemberAccess(object=obj, member=expr.member)
2190
+
2191
+ def _transform_index_access(self, expr: IndexAccess) -> Expression:
2192
+ """Transform index access (Rule 52: warn about negative indices).
2193
+
2194
+ When the object is a FunctionCall (e.g. ta.lowest(ohlc4, 50)[10]),
2195
+ hoist the call into a temp Series variable so [N] works at runtime.
2196
+ """
2197
+ obj = self._transform_expression(expr.object)
2198
+ index = self._transform_expression(expr.index)
2199
+
2200
+ # Check for negative index (Rule 52)
2201
+ if isinstance(index, UnaryOp) and index.op == '-':
2202
+ pass
2203
+
2204
+ # Hoist function calls used with history access [N]
2205
+ if isinstance(obj, FunctionCall):
2206
+ if not hasattr(self, '_hoisted_counter'):
2207
+ self._hoisted_counter = 0
2208
+ self._hoisted_counter += 1
2209
+ temp_name = f'_series_{self._hoisted_counter}'
2210
+ # Store hoisted assignment to be prepended by caller
2211
+ if not hasattr(self, '_hoisted_stmts'):
2212
+ self._hoisted_stmts = []
2213
+ self._hoisted_stmts.append(Assignment(
2214
+ target=temp_name, value=obj, type_hint='Series[float]'
2215
+ ))
2216
+ self.import_resolver.uses_series = True
2217
+ return IndexAccess(object=Identifier(name=temp_name), index=index)
2218
+
2219
+ return IndexAccess(object=obj, index=index)
2220
+
2221
+ def _transform_identifier(self, expr: Identifier) -> Identifier:
2222
+ """Transform identifier (Rule 23 for module prefixes)."""
2223
+ # Handle true/false conversion (Rule 17)
2224
+ if expr.name == 'true':
2225
+ return Literal(value=True, literal_type='bool')
2226
+ elif expr.name == 'false':
2227
+ return Literal(value=False, literal_type='bool')
2228
+
2229
+ return Identifier(name=expr.name)
2230
+
2231
+ def _transform_literal(self, expr: Literal) -> Literal:
2232
+ """Transform literal (Rule 17: true/false to True/False)."""
2233
+ if expr.literal_type == 'bool':
2234
+ # Ensure Python boolean values
2235
+ return Literal(value=bool(expr.value), literal_type='bool')
2236
+ return expr
2237
+
2238
+ def _transform_na_literal(self, expr: NaLiteral) -> Identifier:
2239
+ """Transform na literal to na identifier (Rule 19)."""
2240
+ # In PyneCore, 'na' is used directly, not NA()
2241
+ return Identifier(name='na')
2242
+
2243
+ def _extract_last_expression_from_block(self, block):
2244
+ """Extract the last expression from a block of statements.
2245
+ In Pine Script, the last evaluated expression in a block is the return value.
2246
+ """
2247
+ if isinstance(block, Expression):
2248
+ return self._transform_expression(block)
2249
+ if isinstance(block, list) and block:
2250
+ last = block[-1]
2251
+ # If the last item is an expression statement (ExpressionStatement or raw Expression)
2252
+ if isinstance(last, Expression):
2253
+ return self._transform_expression(last)
2254
+ # If it's an Assignment, the value becomes the return
2255
+ if hasattr(last, 'value') and isinstance(last.value, Expression):
2256
+ return self._transform_expression(last.value)
2257
+ return Literal(value=None, literal_type='none')
2258
+
2259
+ def _transform_if_expression(self, expr: IfExpression) -> TernaryOp:
2260
+ """Transform if expression to Python inline if/else (Rule 16)."""
2261
+ # if cond\n val1\nelse\n val2 => val1 if cond else val2
2262
+ condition = self._transform_expression(expr.condition)
2263
+
2264
+ # Handle single expression vs block
2265
+ true_expr = self._extract_last_expression_from_block(expr.true_expr)
2266
+
2267
+ if expr.false_expr:
2268
+ false_expr = self._extract_last_expression_from_block(expr.false_expr)
2269
+ else:
2270
+ false_expr = None
2271
+
2272
+ return TernaryOp(condition=condition, true_expr=true_expr, false_expr=false_expr)
2273
+
2274
+ def _transform_switch_expression(self, expr: SwitchExpression) -> Expression:
2275
+ """Transform switch expression to nested ternary (if-elif-else chain)."""
2276
+ # Transform the switch expression to a chain of ternary operators
2277
+ # switch expr
2278
+ # val1 => result1
2279
+ # val2 => result2
2280
+ # => default
2281
+ # Becomes: result1 if (expr == val1) else (result2 if (expr == val2) else default)
2282
+
2283
+ # Or for switch without expr (condition-based):
2284
+ # switch
2285
+ # cond1 => result1
2286
+ # cond2 => result2
2287
+ # => default
2288
+ # Becomes: result1 if cond1 else (result2 if cond2 else default)
2289
+
2290
+ if not expr.cases:
2291
+ # No cases, just return default
2292
+ if expr.default:
2293
+ return self._extract_last_expression_from_block(expr.default)
2294
+ return Literal(value=None, literal_type='none')
2295
+
2296
+ # Build nested ternary from right to left
2297
+ result = None
2298
+ if expr.default:
2299
+ result = self._extract_last_expression_from_block(expr.default)
2300
+ else:
2301
+ result = Literal(value=None, literal_type='none')
2302
+
2303
+ # Process cases in reverse order to build nested ternary
2304
+ for case_cond, case_value in reversed(expr.cases):
2305
+ # Transform case value
2306
+ value_expr = self._extract_last_expression_from_block(case_value)
2307
+
2308
+ # Build condition
2309
+ if expr.expr:
2310
+ # With expression: (expr == case_cond)
2311
+ condition = BinaryOp(
2312
+ left=self._transform_expression(expr.expr),
2313
+ op='==',
2314
+ right=self._transform_expression(case_cond)
2315
+ )
2316
+ else:
2317
+ # Without expression: just use case_cond as boolean
2318
+ condition = self._transform_expression(case_cond)
2319
+
2320
+ # Build ternary: value if condition else result
2321
+ result = TernaryOp(
2322
+ condition=condition,
2323
+ true_expr=value_expr,
2324
+ false_expr=result
2325
+ )
2326
+
2327
+ return result