opencode-pine2pyne 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opencode_pine2pyne-0.1.0.dist-info/METADATA +123 -0
- opencode_pine2pyne-0.1.0.dist-info/RECORD +23 -0
- opencode_pine2pyne-0.1.0.dist-info/WHEEL +5 -0
- opencode_pine2pyne-0.1.0.dist-info/entry_points.txt +2 -0
- opencode_pine2pyne-0.1.0.dist-info/licenses/LICENSE +201 -0
- opencode_pine2pyne-0.1.0.dist-info/top_level.txt +1 -0
- pine2pyne/README.md +178 -0
- pine2pyne/TRANSPILER_BEST_PRACTICES.md +425 -0
- pine2pyne/TRANSPILER_USAGE.md +264 -0
- pine2pyne/__init__.py +86 -0
- pine2pyne/__main__.py +12 -0
- pine2pyne/ast_nodes.py +352 -0
- pine2pyne/cli.py +212 -0
- pine2pyne/codegen.py +836 -0
- pine2pyne/errors.py +48 -0
- pine2pyne/import_resolver.py +407 -0
- pine2pyne/lexer.py +546 -0
- pine2pyne/parser.py +1510 -0
- pine2pyne/pine_builtins.py +464 -0
- pine2pyne/symbol_table.py +156 -0
- pine2pyne/tokens.py +143 -0
- pine2pyne/transformer.py +2327 -0
- pine2pyne/type_inference.py +328 -0
pine2pyne/transformer.py
ADDED
|
@@ -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
|