pine2pyne 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pine2pyne/__init__.py +84 -0
- pine2pyne/__main__.py +12 -0
- pine2pyne/ast_nodes.py +405 -0
- pine2pyne/ast_walk.py +93 -0
- pine2pyne/block_value_lowering.py +694 -0
- pine2pyne/bool_builtins.py +42 -0
- pine2pyne/bool_value_typing.py +580 -0
- pine2pyne/cli.py +247 -0
- pine2pyne/codegen.py +886 -0
- pine2pyne/compat/__init__.py +19 -0
- pine2pyne/compat/__main__.py +6 -0
- pine2pyne/compat/cli.py +114 -0
- pine2pyne/compat/comparison.py +105 -0
- pine2pyne/compat/corpus.py +73 -0
- pine2pyne/compat/report.py +95 -0
- pine2pyne/compat/runner.py +63 -0
- pine2pyne/condition_typing.py +260 -0
- pine2pyne/conditional_history_calls.py +96 -0
- pine2pyne/core/__init__.py +26 -0
- pine2pyne/core/contracts.py +125 -0
- pine2pyne/di.py +173 -0
- pine2pyne/emit/__init__.py +5 -0
- pine2pyne/emit/legacy.py +14 -0
- pine2pyne/emit/pynecomp/__init__.py +20 -0
- pine2pyne/emit/pynecomp/banner.py +30 -0
- pine2pyne/emit/pynecomp/blank_lines.py +69 -0
- pine2pyne/emit/pynecomp/emitter.py +61 -0
- pine2pyne/emit/pynecomp/imports.py +65 -0
- pine2pyne/emit/pynecomp/passes/__init__.py +84 -0
- pine2pyne/emit/pynecomp/passes/annotations.py +422 -0
- pine2pyne/emit/pynecomp/passes/base.py +69 -0
- pine2pyne/emit/pynecomp/passes/builtins.py +169 -0
- pine2pyne/emit/pynecomp/passes/hoisting.py +205 -0
- pine2pyne/emit/pynecomp/passes/imports.py +112 -0
- pine2pyne/emit/pynecomp/passes/legacy_colors.py +101 -0
- pine2pyne/emit/pynecomp/passes/library.py +205 -0
- pine2pyne/emit/pynecomp/passes/rewrites.py +215 -0
- pine2pyne/emit/pynecomp/printer.py +288 -0
- pine2pyne/emit/pynecomp/unparser.py +186 -0
- pine2pyne/emitted_names.py +152 -0
- pine2pyne/errors.py +69 -0
- pine2pyne/for_loop_end_bound.py +172 -0
- pine2pyne/grouped_history_lowering.py +310 -0
- pine2pyne/import_resolver.py +469 -0
- pine2pyne/input_hoisting.py +195 -0
- pine2pyne/legacy_function_names.py +74 -0
- pine2pyne/legacy_sessions.py +68 -0
- pine2pyne/lexer.py +630 -0
- pine2pyne/library_collision.py +434 -0
- pine2pyne/namespace_members.py +247 -0
- pine2pyne/node_ids.py +235 -0
- pine2pyne/once_lowering.py +75 -0
- pine2pyne/overload_resolution.py +475 -0
- pine2pyne/parse_repair.py +398 -0
- pine2pyne/parser.py +1873 -0
- pine2pyne/pine_builtins.py +394 -0
- pine2pyne/pine_semantics.py +604 -0
- pine2pyne/pipeline/__init__.py +18 -0
- pine2pyne/pipeline/service.py +70 -0
- pine2pyne/pipeline/stages.py +73 -0
- pine2pyne/scope_renamer.py +965 -0
- pine2pyne/strategy_when_lowering.py +101 -0
- pine2pyne/strict_boolean.py +101 -0
- pine2pyne/symbol_table.py +166 -0
- pine2pyne/tokens.py +143 -0
- pine2pyne/transformer.py +2510 -0
- pine2pyne/type_inference.py +422 -0
- pine2pyne/udt_field_defaults.py +149 -0
- pine2pyne/value_blocks.py +357 -0
- pine2pyne-0.2.4.dist-info/METADATA +99 -0
- pine2pyne-0.2.4.dist-info/RECORD +75 -0
- pine2pyne-0.2.4.dist-info/WHEEL +5 -0
- pine2pyne-0.2.4.dist-info/entry_points.txt +3 -0
- pine2pyne-0.2.4.dist-info/licenses/LICENSE +201 -0
- pine2pyne-0.2.4.dist-info/top_level.txt +1 -0
pine2pyne/transformer.py
ADDED
|
@@ -0,0 +1,2510 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AST Transformer: Pine Script AST → PyneCore-compatible AST.
|
|
3
|
+
|
|
4
|
+
Implements all 54 transformation rules from the transpiler specification.
|
|
5
|
+
"""
|
|
6
|
+
import dataclasses
|
|
7
|
+
import re
|
|
8
|
+
from typing import List, Dict, Any, Union, Optional
|
|
9
|
+
from .ast_nodes import *
|
|
10
|
+
from .symbol_table import SymbolTable, Symbol, VariableKind
|
|
11
|
+
from .type_inference import TypeInference, receiver_path
|
|
12
|
+
from .ast_walk import walk
|
|
13
|
+
from .block_value_lowering import bool_is_never_na, keeps_three_state_bool, lower_block_values
|
|
14
|
+
from .bool_value_typing import has_untaken_bool_switch, reads_na_of_bool, stamp_bool_values
|
|
15
|
+
from .condition_typing import ConditionTyping
|
|
16
|
+
from .grouped_history_lowering import lower_grouped_history
|
|
17
|
+
from .for_loop_end_bound import mark_dynamic_end_bounds
|
|
18
|
+
from .emitted_names import EMITTED_HELPER_NAMES, free_spelling, identifier_names
|
|
19
|
+
from .scope_renamer import collect_names, fresh_name, rename_shadowing_locals
|
|
20
|
+
from .strict_boolean import evaluates_and_or_strictly
|
|
21
|
+
from .overload_resolution import OverloadArgumentCoercion, mark_overloaded_declarations
|
|
22
|
+
from .udt_field_defaults import constant_heads, refuse_invalid_defaults
|
|
23
|
+
from .import_resolver import ImportResolver
|
|
24
|
+
from .pine_builtins import (
|
|
25
|
+
get_function_name, get_module_name, get_type_name,
|
|
26
|
+
get_plot_style_remap, needs_plot_style_remap, is_series_variable,
|
|
27
|
+
is_input_function, TYPE_RENAMES, PYNECORE_LIB_MODULES, DRAWING_ALL_VARIABLES, DRAWING_CAST_FUNCTIONS,
|
|
28
|
+
method_namespaces, bind_input_positional_args, SERIES_VARIABLES,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Known module names - used to guard against transforming module.function() calls
|
|
32
|
+
# as if they were chained member access (e.g. array.size != obj.size)
|
|
33
|
+
KNOWN_MODULES = PYNECORE_LIB_MODULES | {
|
|
34
|
+
'bar_index', 'close', 'open', 'high', 'low', 'volume', 'time',
|
|
35
|
+
'hl2', 'hlc3', 'ohlc4', 'hlcc4',
|
|
36
|
+
'dayofmonth', 'dayofweek', 'hour', 'minute', 'month', 'second',
|
|
37
|
+
'weekofyear', 'year', 'timenow',
|
|
38
|
+
'adjustment', 'earnings', 'dividends', 'splits',
|
|
39
|
+
'order', 'str',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
RANGE_HELPER = 'pine_range' # the runtime's fixed-bound loop helper (card #94)
|
|
43
|
+
|
|
44
|
+
# The Python type hints of a Pine map (`map<K, V>` -> `dict[K, V]`): `for [k, v]`
|
|
45
|
+
# over one reads its key/value pairs, not index/element (card #17)
|
|
46
|
+
MAP_TYPE_PREFIXES = ('dict',)
|
|
47
|
+
# The type hints of every collection a for-in can iterate: array, matrix, map
|
|
48
|
+
COLLECTION_TYPE_PREFIXES = ('list', 'Matrix') + MAP_TYPE_PREFIXES
|
|
49
|
+
|
|
50
|
+
# pynecore's runtime method dispatcher: `method_call('m', receiver, ...)` runs the
|
|
51
|
+
# namespace function of the receiver's class (card #8)
|
|
52
|
+
RUNTIME_METHOD_DISPATCH = 'method_call'
|
|
53
|
+
# array.new<T>(...) element types with a dedicated pynecore factory
|
|
54
|
+
ARRAY_FACTORIES = {
|
|
55
|
+
'string': 'array.new_string',
|
|
56
|
+
'int': 'array.new_int',
|
|
57
|
+
'float': 'array.new_float',
|
|
58
|
+
'bool': 'array.new_bool',
|
|
59
|
+
'color': 'array.new_color',
|
|
60
|
+
'line': 'array.new_line',
|
|
61
|
+
'label': 'array.new_label',
|
|
62
|
+
'box': 'array.new_box',
|
|
63
|
+
'table': 'array.new_table',
|
|
64
|
+
'linefill': 'array.new_linefill', # PyneComp writes `array.new_linefill()` (probe fp7)
|
|
65
|
+
}
|
|
66
|
+
ARRAY_FACTORY_ELEMENT_TYPES = frozenset(ARRAY_FACTORIES)
|
|
67
|
+
# Built-in object types without a factory: array.new<T>(size, value) goes through array.new (card #81)
|
|
68
|
+
BUILTIN_OBJECT_ELEMENT_TYPES = frozenset({'chart.point', 'polyline'})
|
|
69
|
+
# How a method call reaches a user method (Transformer._user_method_route)
|
|
70
|
+
USER_METHOD = 'user method'
|
|
71
|
+
RUNTIME_CHOICE = 'user method or builtin, by the receiver class'
|
|
72
|
+
from .errors import TransformerError, UnsupportedFeatureError
|
|
73
|
+
from .value_blocks import lower_value_blocks
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Builtins whose first parameter is a condition -> its keyword name
|
|
77
|
+
_CONDITION_PARAMETERS = {'ta.valuewhen': 'condition', 'ta.barssince': 'condition', 'alertcondition': 'condition'}
|
|
78
|
+
|
|
79
|
+
# Pine type casts of na -> the Python type of the typed na, NA(T)
|
|
80
|
+
_NA_CAST_TYPES = {
|
|
81
|
+
'int': 'int', 'float': 'float', 'bool': 'bool', 'string': 'str', 'color': 'Color',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _spelled_type(type_name: Optional[str]) -> str:
|
|
86
|
+
"""A Pine or Python type in one spelling: `array<float>` and `list[float]` are the same."""
|
|
87
|
+
return convert_pine_generic_to_python(type_name).replace(' ', '') if type_name else ''
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class PyneTransformedScript:
|
|
91
|
+
"""Holds the transformed script ready for code generation."""
|
|
92
|
+
|
|
93
|
+
def __init__(self):
|
|
94
|
+
self.imports: List[str] = []
|
|
95
|
+
self.type_declarations: List[TypeDecl] = [] # Custom type definitions
|
|
96
|
+
self.enum_declarations: List[EnumDecl] = [] # Enum definitions
|
|
97
|
+
self.helper_functions: List[FuncDecl] = []
|
|
98
|
+
self.main_decorator: Optional[Union[IndicatorDecl, StrategyDecl]] = None
|
|
99
|
+
self.main_params: List[Parameter] = []
|
|
100
|
+
self.main_body: List[Statement] = []
|
|
101
|
+
self.global_vars: List[Union[VarDecl, Assignment]] = []
|
|
102
|
+
self.module_constants: List[Assignment] = [] # Pre-main literal constants
|
|
103
|
+
self.strict_boolean_operators = False # Pine v5 and earlier: and/or evaluate both operands (card #65)
|
|
104
|
+
self.bool_is_never_na = True # False for Pine v5 and earlier, where a UDT bool field without a default is na (card #76)
|
|
105
|
+
self.shadowed_namespaces: Dict[str, str] = {} # built-in namespace alias of a library -> its module name
|
|
106
|
+
self.reads_na_of_bool = False # Pine v5: a named bool is read with history or by na() (PyneComp's na_bool)
|
|
107
|
+
self.pine_version = 6 # the script's //@version
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def convert_pine_generic_to_python(pine_type: str) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Convert Pine Script generic types to Python type hints.
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
map<string, float> -> dict[str, float]
|
|
116
|
+
array<int> -> list[int]
|
|
117
|
+
"""
|
|
118
|
+
# Check if it contains generic syntax
|
|
119
|
+
if '<' not in pine_type or '>' not in pine_type:
|
|
120
|
+
return get_type_name(pine_type)
|
|
121
|
+
|
|
122
|
+
# Extract base type and parameters
|
|
123
|
+
if '<' in pine_type:
|
|
124
|
+
base_type = pine_type[:pine_type.index('<')]
|
|
125
|
+
params_str = pine_type[pine_type.index('<')+1:pine_type.rindex('>')]
|
|
126
|
+
params = [p.strip() for p in params_str.split(',')]
|
|
127
|
+
|
|
128
|
+
# Convert base type
|
|
129
|
+
if base_type == 'map':
|
|
130
|
+
base_type = 'dict'
|
|
131
|
+
elif base_type == 'array':
|
|
132
|
+
base_type = 'list'
|
|
133
|
+
else:
|
|
134
|
+
# For other types like matrix, line, etc., use type name mapping
|
|
135
|
+
base_type = get_type_name(base_type)
|
|
136
|
+
|
|
137
|
+
# Convert parameter types
|
|
138
|
+
converted_params = [get_type_name(p) for p in params]
|
|
139
|
+
|
|
140
|
+
return f"{base_type}[{', '.join(converted_params)}]"
|
|
141
|
+
|
|
142
|
+
return get_type_name(pine_type)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Transformer:
|
|
146
|
+
"""Transforms Pine Script AST to PyneCore-compatible form.
|
|
147
|
+
|
|
148
|
+
One instance transforms ONE script (card #83): inputs, symbols, import state and method
|
|
149
|
+
maps live on the instance, so a second transform() raises TransformerError. Build a fresh
|
|
150
|
+
Transformer per script, as transpile() does.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(self):
|
|
154
|
+
self._transform_started = False # set by transform() before any work; see transform()
|
|
155
|
+
self.symbol_table = SymbolTable()
|
|
156
|
+
self.type_inference = TypeInference(self.symbol_table)
|
|
157
|
+
self.import_resolver = ImportResolver()
|
|
158
|
+
self.input_declarations: List[InputDecl] = []
|
|
159
|
+
self.current_function_params: Dict[str, str] = {} # Track current function parameters
|
|
160
|
+
self.current_function_shadowed_names: set = set() # Untyped parameters that also name a global
|
|
161
|
+
self.bool_is_never_na = True # Pine v6 and later; set per script by transform()
|
|
162
|
+
self.pine_version = 6 # set per script by transform()
|
|
163
|
+
self.three_state_bool = False # Pine v4 and v5: the decorator sets na_bool; set per script by transform()
|
|
164
|
+
self.type_inference.current_scope = lambda: (self.current_function_params, self.current_function_shadowed_names)
|
|
165
|
+
# Each @method function name -> the Python types of its `this` parameter
|
|
166
|
+
self.method_functions: Dict[str, set] = {}
|
|
167
|
+
self._renamed_functions: Dict[str, str] = {} # Pine name -> sanitized Python name
|
|
168
|
+
self._pine_keywords: Dict[str, Dict[str, str]] = {} # set by transform()
|
|
169
|
+
# A switch-subject temporary avoids every Pine name of the script; the
|
|
170
|
+
# names are collected on first need (card #43)
|
|
171
|
+
self._names_source: Optional[Script] = None
|
|
172
|
+
self._object_element_types: Optional[set] = None # array.new<T> element types (card #81)
|
|
173
|
+
self._used_names: Optional[set] = None
|
|
174
|
+
self._range_helper: Optional[str] = None # the name pine_range is imported under (card #94)
|
|
175
|
+
# Identifiers of the script and the names the module defines itself: what a generated
|
|
176
|
+
# spelling (the tuple temporary, a hidden source parameter) must not take (card #77)
|
|
177
|
+
self._identifier_names: Optional[set] = None
|
|
178
|
+
# float(...) for a float-typed argument where int and float overloads meet (card #39)
|
|
179
|
+
self._overload_arguments = OverloadArgumentCoercion(
|
|
180
|
+
Script(), self._visit_ast_nodes, self._receiver_type, self.symbol_table.udt_field_types)
|
|
181
|
+
|
|
182
|
+
# PRIORITY 1 FIX: Track Series variables in user-defined functions
|
|
183
|
+
self.in_user_function = False # Are we inside a user-defined function?
|
|
184
|
+
self.series_variables: set = set() # Variables that need to be Series objects
|
|
185
|
+
self.function_local_vars: set = set() # All local variables in current function
|
|
186
|
+
# Names the scope being emitted (main, or the current function) reads with `[n]` (card #48)
|
|
187
|
+
self.names_read_with_history: set = set()
|
|
188
|
+
|
|
189
|
+
def _visit_ast_nodes(self, nodes: list, visitor_fn) -> None:
|
|
190
|
+
"""Call visitor_fn on every AST node, parents first. The shared walk also
|
|
191
|
+
enters else-if arms and switch cases, which are tuples (card #44)."""
|
|
192
|
+
for node in walk(nodes):
|
|
193
|
+
visitor_fn(node)
|
|
194
|
+
|
|
195
|
+
def _function_shadowed_names(self, func: FuncDecl) -> set:
|
|
196
|
+
"""Untyped parameters, and locals, that also name a global.
|
|
197
|
+
|
|
198
|
+
The transformer types such a name from the GLOBAL symbol, so a field path
|
|
199
|
+
on it must not be typed from there. A function local keeps the name of a global
|
|
200
|
+
it hides (PyneComp does not rename it): it is typed from its own value only.
|
|
201
|
+
"""
|
|
202
|
+
names = {param.name for param in func.params
|
|
203
|
+
if not param.type_hint and self.symbol_table.lookup_global(param.name)}
|
|
204
|
+
body = func.body if isinstance(func.body, list) else []
|
|
205
|
+
for node in walk(body):
|
|
206
|
+
if isinstance(node, Assignment):
|
|
207
|
+
bound = node.target.names if isinstance(node.target, TupleDestructure) else [node.target]
|
|
208
|
+
elif isinstance(node, (VarDecl, VaripDecl)):
|
|
209
|
+
bound = [node.name]
|
|
210
|
+
elif isinstance(node, ForLoop):
|
|
211
|
+
bound = [node.var]
|
|
212
|
+
elif isinstance(node, ForInLoop):
|
|
213
|
+
bound = list(node.vars)
|
|
214
|
+
else:
|
|
215
|
+
continue
|
|
216
|
+
names.update(name for name in bound if isinstance(name, str) and self.symbol_table.lookup_global(name))
|
|
217
|
+
return names
|
|
218
|
+
|
|
219
|
+
def _collect_reassignment_vars(self, script: Script) -> set:
|
|
220
|
+
"""Collect variable names that are reassigned with := operator."""
|
|
221
|
+
reassign_vars = set()
|
|
222
|
+
|
|
223
|
+
def visitor(node):
|
|
224
|
+
if isinstance(node, Reassignment):
|
|
225
|
+
reassign_vars.add(node.target)
|
|
226
|
+
|
|
227
|
+
self._visit_ast_nodes(list(script.declarations) + list(script.body), visitor)
|
|
228
|
+
return reassign_vars
|
|
229
|
+
|
|
230
|
+
def _find_series_variables_in_function(self, func_body: List[Statement]) -> set:
|
|
231
|
+
"""Find variables in a function that need to be Series objects.
|
|
232
|
+
A variable needs to be Series if it's reassigned with := or indexed with [n].
|
|
233
|
+
"""
|
|
234
|
+
series_vars = set()
|
|
235
|
+
|
|
236
|
+
def visitor(node):
|
|
237
|
+
if isinstance(node, Reassignment):
|
|
238
|
+
series_vars.add(node.target)
|
|
239
|
+
if isinstance(node, IndexAccess):
|
|
240
|
+
if isinstance(node.object, Identifier):
|
|
241
|
+
series_vars.add(node.object.name)
|
|
242
|
+
|
|
243
|
+
self._visit_ast_nodes(func_body, visitor)
|
|
244
|
+
return series_vars
|
|
245
|
+
|
|
246
|
+
def transform(self, script: Script) -> PyneTransformedScript:
|
|
247
|
+
"""Transform entire script. Single use: a second call raises (card #83)."""
|
|
248
|
+
if self._transform_started:
|
|
249
|
+
raise TransformerError(
|
|
250
|
+
'a Transformer transforms one script and keeps its state; create a fresh Transformer '
|
|
251
|
+
'for the next script (pine2pyne.transpile() does)')
|
|
252
|
+
self._transform_started = True # before any work, so a failed transform consumes the instance too
|
|
253
|
+
output = PyneTransformedScript()
|
|
254
|
+
output.strict_boolean_operators = evaluates_and_or_strictly(script)
|
|
255
|
+
version_text = getattr(script.version, 'version', '') if script.version else ''
|
|
256
|
+
self.pine_version = int(version_text) if str(version_text).isdigit() else 6
|
|
257
|
+
output.bool_is_never_na = bool_is_never_na(script)
|
|
258
|
+
|
|
259
|
+
# A Pine local that shadows an outer name gets its own name (card #38),
|
|
260
|
+
# so every stage below sees one binding per name
|
|
261
|
+
# Pine v6: an untaken if/switch of bool type is false, not na (card #50)
|
|
262
|
+
self.bool_is_never_na = bool_is_never_na(script)
|
|
263
|
+
self.three_state_bool = keeps_three_state_bool(script)
|
|
264
|
+
# A binding named like a pynecore.lib name the script also uses gets its
|
|
265
|
+
# own name; a namespace under an input or export that keeps it gets an alias (card #9)
|
|
266
|
+
# if/switch values become statements around __block_result__ temporaries (PyneComp)
|
|
267
|
+
# Pine v5: a switch without a default that gives a bool may be na (before the switches are lowered)
|
|
268
|
+
untaken_bool_switch = self.pine_version == 5 and has_untaken_bool_switch(script)
|
|
269
|
+
script = lower_value_blocks(script)
|
|
270
|
+
renamed = rename_shadowing_locals(script)
|
|
271
|
+
script = renamed.script
|
|
272
|
+
# Pine v5: PyneComp marks `na_bool=True` only where a named bool is read with history or by na()
|
|
273
|
+
output.pine_version = self.pine_version
|
|
274
|
+
output.reads_na_of_bool = (untaken_bool_switch or reads_na_of_bool(script)) if self.pine_version >= 5 else False
|
|
275
|
+
# which values PyneComp reads as numbers in a condition (cast_bool)
|
|
276
|
+
self._condition_typing = ConditionTyping(script)
|
|
277
|
+
self.import_resolver.aliased_modules = dict(renamed.namespace_aliases)
|
|
278
|
+
# user function or method name -> {emitted parameter name: its Pine name}:
|
|
279
|
+
# a builtin method of the same name is called with the Pine keyword (card #17)
|
|
280
|
+
self._pine_keywords = {name: {emitted: pine for pine, emitted in renames.items()}
|
|
281
|
+
for name, renames in renamed.parameter_names.items()}
|
|
282
|
+
# Pine v6: a for loop whose end may change in its body re-reads it on
|
|
283
|
+
# every pass; marked before the lowering, which refuses a history site there (card #82)
|
|
284
|
+
script = mark_dynamic_end_bounds(script)
|
|
285
|
+
# A history index on an expression reads a named local (card #32). It
|
|
286
|
+
# runs first: a branch it turns into a block is lowered by the next pass
|
|
287
|
+
# PyneComp reads the history of an expression with inline_series(expr, n): no lowering
|
|
288
|
+
# (`_transform_index_access`); the grouped-history lowering of card #32 is not run
|
|
289
|
+
# Pine v6: the bool type of every if/switch/loop value and function,
|
|
290
|
+
# decided once before the lowering rewrites them (card #56)
|
|
291
|
+
if self.bool_is_never_na:
|
|
292
|
+
script = stamp_bool_values(script)
|
|
293
|
+
# A multi-statement branch of an if/switch used as a value becomes
|
|
294
|
+
# statements that assign the value (card #41)
|
|
295
|
+
script = lower_block_values(script)
|
|
296
|
+
self._names_source, self._used_names, self._range_helper = script, None, None
|
|
297
|
+
self._object_element_types = None # array.new<T> element types, read on first use (card #81)
|
|
298
|
+
self._identifier_names = None
|
|
299
|
+
|
|
300
|
+
# Build symbol table
|
|
301
|
+
self._build_symbol_table(script)
|
|
302
|
+
|
|
303
|
+
# Infer types
|
|
304
|
+
self.type_inference.infer_types(script)
|
|
305
|
+
self._overload_arguments = OverloadArgumentCoercion(
|
|
306
|
+
script, self._visit_ast_nodes, self._receiver_type, self.symbol_table.udt_field_types)
|
|
307
|
+
|
|
308
|
+
# (Removed: _history_ref_vars replaced by symbol.is_indexed from type inference)
|
|
309
|
+
|
|
310
|
+
# Collect variables that use := (reassignment)
|
|
311
|
+
self._reassignment_vars = self._collect_reassignment_vars(script)
|
|
312
|
+
self.names_read_with_history = self._main_scope_history_reads(script)
|
|
313
|
+
|
|
314
|
+
# Analyze imports
|
|
315
|
+
self.import_resolver.analyze(script)
|
|
316
|
+
|
|
317
|
+
# Transform script declaration (indicator/strategy)
|
|
318
|
+
if script.script_decl:
|
|
319
|
+
output.main_decorator = self._transform_script_declaration(script.script_decl)
|
|
320
|
+
|
|
321
|
+
# Extract input declarations to main() parameters
|
|
322
|
+
self._extract_input_declarations(script)
|
|
323
|
+
output.main_params = self._create_main_parameters()
|
|
324
|
+
|
|
325
|
+
# Transform type and enum declarations - stay at module level
|
|
326
|
+
heads = constant_heads(script)
|
|
327
|
+
for decl in script.declarations:
|
|
328
|
+
if isinstance(decl, TypeDecl):
|
|
329
|
+
refuse_invalid_defaults(decl, heads) # only literals and built-ins (card #17)
|
|
330
|
+
output.type_declarations.append(decl) # Keep as-is for codegen
|
|
331
|
+
elif isinstance(decl, EnumDecl):
|
|
332
|
+
output.enum_declarations.append(decl) # Keep as-is for codegen
|
|
333
|
+
|
|
334
|
+
# Identify body assignments that must be module-level constants.
|
|
335
|
+
# In Pine Script, declarations before the first function call (like
|
|
336
|
+
# `const string G_ENTRY = '...'` or `DEFAULT_PYRAMIDING = 6`) can be
|
|
337
|
+
# referenced by input.* group= params and strategy() kwargs, which
|
|
338
|
+
# become main() parameter defaults evaluated at function definition time.
|
|
339
|
+
# These must be emitted at module level, not inside main().
|
|
340
|
+
module_const_names = self._collect_module_constant_names(script)
|
|
341
|
+
|
|
342
|
+
# Merge all non-type, non-input declarations with body statements
|
|
343
|
+
# sorted by source line number to preserve original Pine Script ordering
|
|
344
|
+
all_items: List[tuple] = []
|
|
345
|
+
for decl in script.declarations:
|
|
346
|
+
if isinstance(decl, (VarDecl, VaripDecl)):
|
|
347
|
+
all_items.append(('var', decl))
|
|
348
|
+
elif isinstance(decl, FuncDecl):
|
|
349
|
+
all_items.append(('func', decl))
|
|
350
|
+
# TypeDecl and InputDecl are handled separately above
|
|
351
|
+
|
|
352
|
+
for stmt in script.body:
|
|
353
|
+
all_items.append(('body', stmt))
|
|
354
|
+
|
|
355
|
+
# Sort by line number to preserve source order
|
|
356
|
+
all_items.sort(key=lambda x: x[1].line)
|
|
357
|
+
|
|
358
|
+
# Process all items in source order into main_body
|
|
359
|
+
# Propagate source line numbers for blank line detection in codegen
|
|
360
|
+
for item_type, item in all_items:
|
|
361
|
+
source_line = item.line
|
|
362
|
+
if item_type == 'var':
|
|
363
|
+
# Check if this var declaration should be a module-level constant
|
|
364
|
+
# (module_const_names holds only names whose value may be hoisted)
|
|
365
|
+
if isinstance(item, (VarDecl, VaripDecl)) and item.name in module_const_names:
|
|
366
|
+
# Emit as a plain module-level assignment with only its declared
|
|
367
|
+
# type: pynecore allows a Persistent or Series only inside a function
|
|
368
|
+
transformed = Assignment(
|
|
369
|
+
target=item.name, value=self._transform_expression(item.value),
|
|
370
|
+
type_hint=convert_pine_generic_to_python(item.type_hint) if item.type_hint else None)
|
|
371
|
+
transformed.line = source_line
|
|
372
|
+
output.module_constants.append(transformed)
|
|
373
|
+
continue
|
|
374
|
+
result = self._transform_var_declaration(item)
|
|
375
|
+
result.line = source_line
|
|
376
|
+
output.main_body.append(result)
|
|
377
|
+
elif item_type == 'func':
|
|
378
|
+
result = self._transform_function(item)
|
|
379
|
+
result.line = source_line
|
|
380
|
+
output.main_body.append(result)
|
|
381
|
+
else: # body
|
|
382
|
+
# Check if this is a literal assignment that should be module-level
|
|
383
|
+
if (isinstance(item, Assignment) and
|
|
384
|
+
isinstance(item.target, str) and
|
|
385
|
+
item.target in module_const_names):
|
|
386
|
+
# Emit as module-level constant instead of inside main()
|
|
387
|
+
transformed = self._transform_statement(item)
|
|
388
|
+
if transformed and not isinstance(transformed, list):
|
|
389
|
+
transformed.line = source_line
|
|
390
|
+
output.module_constants.append(transformed)
|
|
391
|
+
continue
|
|
392
|
+
|
|
393
|
+
transformed = self._transform_statement(item)
|
|
394
|
+
if transformed:
|
|
395
|
+
if isinstance(transformed, list):
|
|
396
|
+
for t in transformed:
|
|
397
|
+
t.line = source_line
|
|
398
|
+
output.main_body.extend(transformed)
|
|
399
|
+
else:
|
|
400
|
+
transformed.line = source_line
|
|
401
|
+
output.main_body.append(transformed)
|
|
402
|
+
|
|
403
|
+
# Same-name declarations become pynecore @overload implementations (card #39)
|
|
404
|
+
output.main_body, self.import_resolver.uses_overload = mark_overloaded_declarations(
|
|
405
|
+
output.main_body, {decl.name for decl in output.enum_declarations},
|
|
406
|
+
self.type_inference.declared_type_name)
|
|
407
|
+
|
|
408
|
+
# Analyze transformed AST for type names in type hints (Matrix, Line, etc.)
|
|
409
|
+
self._analyze_transformed_types(output)
|
|
410
|
+
|
|
411
|
+
# Generate imports
|
|
412
|
+
output.imports = self.import_resolver.generate_imports()
|
|
413
|
+
output.shadowed_namespaces = dict(self.import_resolver.shadowed_namespaces or {})
|
|
414
|
+
|
|
415
|
+
return output
|
|
416
|
+
|
|
417
|
+
def _get_func_name_from_call(self, call: FunctionCall) -> Optional[str]:
|
|
418
|
+
"""Extract function name string from a FunctionCall node."""
|
|
419
|
+
if isinstance(call.func, str):
|
|
420
|
+
return call.func
|
|
421
|
+
elif isinstance(call.func, MemberAccess):
|
|
422
|
+
if isinstance(call.func.object, str):
|
|
423
|
+
return f"{call.func.object}.{call.func.member}"
|
|
424
|
+
elif isinstance(call.func.object, Identifier):
|
|
425
|
+
return f"{call.func.object.name}.{call.func.member}"
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
def _analyze_transformed_types(self, output: PyneTransformedScript) -> None:
|
|
429
|
+
"""Analyze transformed AST to extract type names from type hints."""
|
|
430
|
+
import re
|
|
431
|
+
|
|
432
|
+
# Track if Series is actually used
|
|
433
|
+
series_used = False
|
|
434
|
+
persistent_used = False
|
|
435
|
+
|
|
436
|
+
def extract_types_from_hint(type_hint: str) -> None:
|
|
437
|
+
"""Extract type names like Line, Matrix from type hints."""
|
|
438
|
+
nonlocal series_used, persistent_used
|
|
439
|
+
|
|
440
|
+
if not type_hint:
|
|
441
|
+
return
|
|
442
|
+
|
|
443
|
+
# Check for Series and Persistent usage
|
|
444
|
+
# A typeless na initialiser is annotated bare `Series` (card #10);
|
|
445
|
+
# `PersistentSeries` alone needs no `Series` import (card #48)
|
|
446
|
+
if re.search(r'\bSeries\b', type_hint):
|
|
447
|
+
series_used = True
|
|
448
|
+
if re.search(r'\bPersistent\b', type_hint):
|
|
449
|
+
persistent_used = True
|
|
450
|
+
|
|
451
|
+
# Match capital letter type names that might be drawing/structural types
|
|
452
|
+
for match in re.finditer(r'\b([A-Z][a-zA-Z]*)\b', type_hint):
|
|
453
|
+
type_name = match.group(1)
|
|
454
|
+
# Add to import resolver if it's a known type
|
|
455
|
+
self.import_resolver.add_drawing_type(type_name)
|
|
456
|
+
|
|
457
|
+
def scan_statement(stmt: Statement) -> None:
|
|
458
|
+
"""Recursively scan a statement for type hints."""
|
|
459
|
+
# Handle lists (flattened statement lists from hoisting)
|
|
460
|
+
if isinstance(stmt, list):
|
|
461
|
+
for s in stmt:
|
|
462
|
+
scan_statement(s)
|
|
463
|
+
return
|
|
464
|
+
if stmt is None:
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
# Check if statement has type hint
|
|
468
|
+
if hasattr(stmt, 'type_hint') and stmt.type_hint:
|
|
469
|
+
extract_types_from_hint(stmt.type_hint)
|
|
470
|
+
|
|
471
|
+
# Recursively scan nested structures
|
|
472
|
+
if isinstance(stmt, FuncDecl) and hasattr(stmt, 'params'):
|
|
473
|
+
for param in stmt.params:
|
|
474
|
+
if hasattr(param, 'type_hint') and param.type_hint:
|
|
475
|
+
extract_types_from_hint(param.type_hint)
|
|
476
|
+
if hasattr(stmt, 'body') and isinstance(stmt.body, list):
|
|
477
|
+
for body_stmt in stmt.body:
|
|
478
|
+
scan_statement(body_stmt)
|
|
479
|
+
|
|
480
|
+
# Scan all compound statement bodies
|
|
481
|
+
elif isinstance(stmt, IfStatement):
|
|
482
|
+
for body_stmt in (stmt.body or []):
|
|
483
|
+
scan_statement(body_stmt)
|
|
484
|
+
for _, elif_body in (stmt.elseifs or []):
|
|
485
|
+
for body_stmt in elif_body:
|
|
486
|
+
scan_statement(body_stmt)
|
|
487
|
+
for body_stmt in (stmt.else_body or []):
|
|
488
|
+
scan_statement(body_stmt)
|
|
489
|
+
elif isinstance(stmt, (ForLoop, ForInLoop, WhileLoop)):
|
|
490
|
+
for body_stmt in (stmt.body or []):
|
|
491
|
+
scan_statement(body_stmt)
|
|
492
|
+
|
|
493
|
+
# Scan global vars for type hints (var/varip declarations)
|
|
494
|
+
for var in output.global_vars:
|
|
495
|
+
if hasattr(var, 'type_hint') and var.type_hint:
|
|
496
|
+
extract_types_from_hint(var.type_hint)
|
|
497
|
+
|
|
498
|
+
# Scan module constants and the main body for type hints
|
|
499
|
+
for stmt in output.module_constants + output.main_body:
|
|
500
|
+
scan_statement(stmt)
|
|
501
|
+
|
|
502
|
+
# Scan main parameters for type hints
|
|
503
|
+
for param in output.main_params:
|
|
504
|
+
if hasattr(param, 'type_hint') and param.type_hint:
|
|
505
|
+
extract_types_from_hint(param.type_hint)
|
|
506
|
+
|
|
507
|
+
# Update import resolver based on actual usage in transformed code
|
|
508
|
+
self.import_resolver.uses_series = series_used
|
|
509
|
+
self.import_resolver.uses_persistent = persistent_used
|
|
510
|
+
|
|
511
|
+
# ========================================================================
|
|
512
|
+
# Symbol table building
|
|
513
|
+
# ========================================================================
|
|
514
|
+
|
|
515
|
+
def _build_symbol_table(self, script: Script) -> None:
|
|
516
|
+
"""Build symbol table from script declarations."""
|
|
517
|
+
# Record declared UDT field types first: typing a var below (e.g.
|
|
518
|
+
# `var b = t.bx`) and every later method call on a field need them.
|
|
519
|
+
for decl in script.declarations:
|
|
520
|
+
if isinstance(decl, TypeDecl):
|
|
521
|
+
self.symbol_table.udt_field_types[decl.name] = {
|
|
522
|
+
field_name: convert_pine_generic_to_python(field_type)
|
|
523
|
+
for field_name, field_type, *_ in decl.fields if field_type
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
# Register input declarations
|
|
527
|
+
for decl in script.declarations:
|
|
528
|
+
if isinstance(decl, InputDecl):
|
|
529
|
+
symbol = Symbol(
|
|
530
|
+
decl.name,
|
|
531
|
+
VariableKind.INPUT,
|
|
532
|
+
is_global=True
|
|
533
|
+
)
|
|
534
|
+
self.symbol_table.define(symbol)
|
|
535
|
+
|
|
536
|
+
# Register var/varip declarations
|
|
537
|
+
for decl in script.declarations:
|
|
538
|
+
if isinstance(decl, VarDecl):
|
|
539
|
+
type_hint = self._infer_type_from_declaration(decl)
|
|
540
|
+
symbol = Symbol(
|
|
541
|
+
decl.name,
|
|
542
|
+
VariableKind.VAR,
|
|
543
|
+
type_hint=type_hint,
|
|
544
|
+
is_global=True
|
|
545
|
+
)
|
|
546
|
+
self.symbol_table.define(symbol)
|
|
547
|
+
elif isinstance(decl, VaripDecl):
|
|
548
|
+
type_hint = self._infer_type_from_declaration(decl)
|
|
549
|
+
symbol = Symbol(
|
|
550
|
+
decl.name,
|
|
551
|
+
VariableKind.VARIP,
|
|
552
|
+
type_hint=type_hint,
|
|
553
|
+
is_global=True
|
|
554
|
+
)
|
|
555
|
+
self.symbol_table.define(symbol)
|
|
556
|
+
|
|
557
|
+
# Register type declarations (UDTs)
|
|
558
|
+
for decl in script.declarations:
|
|
559
|
+
if isinstance(decl, TypeDecl):
|
|
560
|
+
symbol = Symbol(
|
|
561
|
+
decl.name,
|
|
562
|
+
VariableKind.TYPE,
|
|
563
|
+
is_global=True
|
|
564
|
+
)
|
|
565
|
+
self.symbol_table.define(symbol)
|
|
566
|
+
|
|
567
|
+
# Register function declarations
|
|
568
|
+
for decl in script.declarations:
|
|
569
|
+
if isinstance(decl, FuncDecl):
|
|
570
|
+
symbol = Symbol(
|
|
571
|
+
decl.name,
|
|
572
|
+
VariableKind.FUNCTION,
|
|
573
|
+
is_global=True
|
|
574
|
+
)
|
|
575
|
+
self.symbol_table.define(symbol)
|
|
576
|
+
|
|
577
|
+
# Register assignments as variables (including nested scopes)
|
|
578
|
+
self._register_assignments_recursive(script.body)
|
|
579
|
+
|
|
580
|
+
def _register_assignments_recursive(self, stmts: List[Statement]) -> None:
|
|
581
|
+
"""Recursively register assignments in the symbol table, including nested scopes."""
|
|
582
|
+
for stmt in stmts:
|
|
583
|
+
if isinstance(stmt, Assignment) and isinstance(stmt.target, str):
|
|
584
|
+
if not self.symbol_table.lookup_global(stmt.target):
|
|
585
|
+
if hasattr(stmt, 'type_hint') and stmt.type_hint:
|
|
586
|
+
type_hint = convert_pine_generic_to_python(stmt.type_hint)
|
|
587
|
+
else:
|
|
588
|
+
type_hint = self.type_inference.infer_type_hint(stmt.value)
|
|
589
|
+
symbol = Symbol(
|
|
590
|
+
stmt.target,
|
|
591
|
+
VariableKind.SERIES,
|
|
592
|
+
type_hint=type_hint,
|
|
593
|
+
is_global=True
|
|
594
|
+
)
|
|
595
|
+
symbol.is_series = True
|
|
596
|
+
self.symbol_table.define(symbol)
|
|
597
|
+
# Recurse into nested scopes
|
|
598
|
+
if isinstance(stmt, IfStatement):
|
|
599
|
+
self._register_assignments_recursive(stmt.body)
|
|
600
|
+
for _, elif_body in stmt.elseifs:
|
|
601
|
+
self._register_assignments_recursive(elif_body)
|
|
602
|
+
if stmt.else_body:
|
|
603
|
+
self._register_assignments_recursive(stmt.else_body)
|
|
604
|
+
elif isinstance(stmt, SwitchStatement):
|
|
605
|
+
for _, case_body in stmt.cases:
|
|
606
|
+
self._register_assignments_recursive(case_body)
|
|
607
|
+
if stmt.default:
|
|
608
|
+
self._register_assignments_recursive(stmt.default)
|
|
609
|
+
elif isinstance(stmt, (ForLoop, ForInLoop, WhileLoop)):
|
|
610
|
+
if isinstance(stmt.body, list):
|
|
611
|
+
self._register_assignments_recursive(stmt.body)
|
|
612
|
+
|
|
613
|
+
def _infer_type_from_declaration(self, decl: Union[VarDecl, VaripDecl]) -> str:
|
|
614
|
+
"""Infer type from var/varip declaration."""
|
|
615
|
+
if decl.type_hint:
|
|
616
|
+
return convert_pine_generic_to_python(decl.type_hint)
|
|
617
|
+
return self.type_inference.infer_type_hint(decl.value)
|
|
618
|
+
|
|
619
|
+
# ========================================================================
|
|
620
|
+
# Module-level constant detection
|
|
621
|
+
# ========================================================================
|
|
622
|
+
|
|
623
|
+
def _collect_identifier_names(self, node: Any) -> set:
|
|
624
|
+
"""Recursively collect all Identifier names referenced in an AST node."""
|
|
625
|
+
names = set()
|
|
626
|
+
if isinstance(node, Identifier):
|
|
627
|
+
names.add(node.name)
|
|
628
|
+
elif isinstance(node, (FunctionCall, MethodCall)):
|
|
629
|
+
for arg in getattr(node, 'args', []):
|
|
630
|
+
names.update(self._collect_identifier_names(arg))
|
|
631
|
+
for val in getattr(node, 'kwargs', {}).values():
|
|
632
|
+
names.update(self._collect_identifier_names(val))
|
|
633
|
+
elif isinstance(node, (BinaryOp,)):
|
|
634
|
+
names.update(self._collect_identifier_names(node.left))
|
|
635
|
+
names.update(self._collect_identifier_names(node.right))
|
|
636
|
+
elif isinstance(node, (UnaryOp,)):
|
|
637
|
+
names.update(self._collect_identifier_names(node.operand))
|
|
638
|
+
elif isinstance(node, (TernaryOp,)):
|
|
639
|
+
names.update(self._collect_identifier_names(node.condition))
|
|
640
|
+
names.update(self._collect_identifier_names(node.true_expr))
|
|
641
|
+
names.update(self._collect_identifier_names(node.false_expr))
|
|
642
|
+
elif isinstance(node, MemberAccess):
|
|
643
|
+
names.update(self._collect_identifier_names(node.object))
|
|
644
|
+
elif isinstance(node, IndexAccess):
|
|
645
|
+
names.update(self._collect_identifier_names(node.object))
|
|
646
|
+
names.update(self._collect_identifier_names(node.index))
|
|
647
|
+
elif isinstance(node, ArrayLiteral):
|
|
648
|
+
for element in node.elements:
|
|
649
|
+
names.update(self._collect_identifier_names(element))
|
|
650
|
+
return names
|
|
651
|
+
|
|
652
|
+
def _collect_module_constant_names(self, script: Script) -> set:
|
|
653
|
+
"""Collect names of body assignments that must be module-level constants.
|
|
654
|
+
|
|
655
|
+
In Pine Script, literal assignments before the first function call
|
|
656
|
+
(e.g., `const string G_ENTRY = '...'`) can be referenced by input.*()
|
|
657
|
+
group= params and strategy/indicator kwargs. Since these become main()
|
|
658
|
+
parameter defaults (evaluated at definition time), the referenced
|
|
659
|
+
variables must exist at module level.
|
|
660
|
+
"""
|
|
661
|
+
# Collect all identifier names referenced in input declarations
|
|
662
|
+
referenced_names = set()
|
|
663
|
+
for decl in script.declarations:
|
|
664
|
+
if isinstance(decl, InputDecl):
|
|
665
|
+
for arg in decl.args:
|
|
666
|
+
referenced_names.update(self._collect_identifier_names(arg))
|
|
667
|
+
for val in decl.kwargs.values():
|
|
668
|
+
referenced_names.update(self._collect_identifier_names(val))
|
|
669
|
+
|
|
670
|
+
# Find body assignments that are literal values AND referenced by inputs
|
|
671
|
+
module_const_names = set()
|
|
672
|
+
for stmt in script.body:
|
|
673
|
+
if (isinstance(stmt, Assignment) and
|
|
674
|
+
isinstance(stmt.target, str) and
|
|
675
|
+
isinstance(stmt.value, Literal) and
|
|
676
|
+
stmt.target in referenced_names):
|
|
677
|
+
module_const_names.add(stmt.target)
|
|
678
|
+
|
|
679
|
+
# Also check var/varip declarations with literal values referenced by inputs
|
|
680
|
+
# (e.g., `var string G_STRATEGY = 'Strategy Settings'` used in group= params)
|
|
681
|
+
for decl in script.declarations:
|
|
682
|
+
if (isinstance(decl, (VarDecl, VaripDecl)) and
|
|
683
|
+
isinstance(decl.value, Literal) and
|
|
684
|
+
decl.name in referenced_names):
|
|
685
|
+
module_const_names.add(decl.name)
|
|
686
|
+
|
|
687
|
+
# a top-level `const` declaration is a module constant (PyneComp, probes const1, const2)
|
|
688
|
+
module_const_names.update(stmt.target for stmt in script.body
|
|
689
|
+
if isinstance(stmt, Assignment) and stmt.is_const and isinstance(stmt.target, str)
|
|
690
|
+
and self._is_constant_expression(stmt.value))
|
|
691
|
+
# strategy/indicator arguments and input arguments, through every constant they are built from
|
|
692
|
+
return (module_const_names | self._declaration_constant_names(script)
|
|
693
|
+
| self._input_constant_names(script))
|
|
694
|
+
|
|
695
|
+
def _declaration_constant_names(self, script: Script) -> set:
|
|
696
|
+
"""Names the script declaration's arguments read, with the constants they are built from.
|
|
697
|
+
|
|
698
|
+
The decorator runs at import, before main(), so each name in its arguments must be a
|
|
699
|
+
module constant: a top-level binding whose value holds only literals, constant
|
|
700
|
+
namespace members (`strategy.fixed`), enum or imported-library members, constant
|
|
701
|
+
functions and other such constants. Anything else is refused here, by name, instead
|
|
702
|
+
of failing or reading an unset value at import (card #55).
|
|
703
|
+
"""
|
|
704
|
+
if not script.script_decl:
|
|
705
|
+
return set()
|
|
706
|
+
roots = {name for value in script.script_decl.kwargs.values() for name in self._read_names(value)}
|
|
707
|
+
return self._constant_closure(script, roots, script.script_decl.line,
|
|
708
|
+
'script declaration argument', 'the declaration needs its value before the script runs',
|
|
709
|
+
unbound_roots_allowed=False)
|
|
710
|
+
|
|
711
|
+
def _input_constant_names(self, script: Script) -> set:
|
|
712
|
+
"""Names the inputs' arguments read, with the constants they are built from (card #78).
|
|
713
|
+
|
|
714
|
+
An input call is a default of main()'s signature, evaluated when the module loads, so a
|
|
715
|
+
top-level constant it reads (`const int LENGTH = BASE * 2`) must be a module constant
|
|
716
|
+
too: the same rule as the declaration's. A name that is no top-level binding (`close`,
|
|
717
|
+
`syminfo.tickerid`, another input) is not read from a constant and is left alone.
|
|
718
|
+
"""
|
|
719
|
+
needed = set()
|
|
720
|
+
bindings = self._top_level_bindings(script)
|
|
721
|
+
for decl in script.declarations:
|
|
722
|
+
if not isinstance(decl, InputDecl):
|
|
723
|
+
continue
|
|
724
|
+
roots = {name for argument in [*decl.args, *decl.kwargs.values()]
|
|
725
|
+
for name in self._read_names(argument)}
|
|
726
|
+
needed |= self._constant_closure(
|
|
727
|
+
script, {name for name in roots if name in bindings}, decl.line,
|
|
728
|
+
f"default of input '{decl.name}'", 'an input default is read when the module loads',
|
|
729
|
+
unbound_roots_allowed=True)
|
|
730
|
+
return needed
|
|
731
|
+
|
|
732
|
+
def _constant_closure(self, script: Script, roots: set, line: int, subject: str, reason: str,
|
|
733
|
+
unbound_roots_allowed: bool) -> set:
|
|
734
|
+
"""The top-level constants `roots` are built from, each checked to be a constant.
|
|
735
|
+
|
|
736
|
+
A name reached through a constant's value must be a constant, a constant namespace
|
|
737
|
+
member or an enum or library member; a root need not be bound (`unbound_roots_allowed`).
|
|
738
|
+
"""
|
|
739
|
+
bindings = self._top_level_bindings(script)
|
|
740
|
+
namespaces = {decl.name for decl in script.declarations if isinstance(decl, EnumDecl)}
|
|
741
|
+
namespaces.update(imported.alias for imported in script.imports if imported.alias)
|
|
742
|
+
pending = [(name, True) for name in sorted(roots)]
|
|
743
|
+
needed = set()
|
|
744
|
+
while pending:
|
|
745
|
+
name, is_root = pending.pop()
|
|
746
|
+
if name in needed or self._is_namespace_constant(name, namespaces):
|
|
747
|
+
continue
|
|
748
|
+
value = bindings.get(name)
|
|
749
|
+
if value is None and is_root and unbound_roots_allowed:
|
|
750
|
+
continue
|
|
751
|
+
if value is None or not self._is_constant_expression(value):
|
|
752
|
+
raise TransformerError(
|
|
753
|
+
f"{subject} uses '{name}', which is not a constant declared "
|
|
754
|
+
f"at the top level; {reason}",
|
|
755
|
+
line)
|
|
756
|
+
needed.add(name)
|
|
757
|
+
pending.extend((read, False) for read in sorted(self._read_names(value)))
|
|
758
|
+
return needed
|
|
759
|
+
|
|
760
|
+
def _top_level_bindings(self, script: Script) -> Dict[str, Any]:
|
|
761
|
+
"""Each top-level `name = value` and var/varip declaration's value, by name."""
|
|
762
|
+
bindings = {decl.name: decl.value for decl in script.declarations
|
|
763
|
+
if isinstance(decl, (VarDecl, VaripDecl))}
|
|
764
|
+
bindings.update({stmt.target: stmt.value for stmt in script.body
|
|
765
|
+
if isinstance(stmt, Assignment) and isinstance(stmt.target, str)})
|
|
766
|
+
return bindings
|
|
767
|
+
|
|
768
|
+
def _read_names(self, node: Any) -> set:
|
|
769
|
+
"""The names `node` reads, except the true/false/na literals."""
|
|
770
|
+
return self._collect_identifier_names(node) - {'true', 'false', 'na'}
|
|
771
|
+
|
|
772
|
+
def _is_namespace_constant(self, name: str, namespaces: set) -> bool:
|
|
773
|
+
"""A member of a constant builtin namespace, of an enum, or of an imported library."""
|
|
774
|
+
from .pine_builtins import DECLARATION_CONSTANT_NAMESPACES
|
|
775
|
+
return '.' in name and (name.startswith(DECLARATION_CONSTANT_NAMESPACES)
|
|
776
|
+
or name.split('.', 1)[0] in namespaces)
|
|
777
|
+
|
|
778
|
+
def _is_constant_expression(self, node: Any) -> bool:
|
|
779
|
+
"""Built only from literals, names, operators and constant functions (the caller checks names)."""
|
|
780
|
+
from .pine_builtins import (DECLARATION_CONSTANT_FUNCTIONS, DECLARATION_CONSTANT_FUNCTION_NAMESPACES,
|
|
781
|
+
DECLARATION_STATEFUL_FUNCTIONS)
|
|
782
|
+
if isinstance(node, (Literal, Identifier)):
|
|
783
|
+
return True
|
|
784
|
+
if isinstance(node, ArrayLiteral):
|
|
785
|
+
return all(self._is_constant_expression(element) for element in node.elements)
|
|
786
|
+
if isinstance(node, FunctionCall):
|
|
787
|
+
constant_function = isinstance(node.func, str) and (
|
|
788
|
+
node.func in DECLARATION_CONSTANT_FUNCTIONS
|
|
789
|
+
or (node.func.startswith(DECLARATION_CONSTANT_FUNCTION_NAMESPACES)
|
|
790
|
+
and node.func not in DECLARATION_STATEFUL_FUNCTIONS))
|
|
791
|
+
return constant_function and all(
|
|
792
|
+
self._is_constant_expression(argument) for argument in [*node.args, *node.kwargs.values()])
|
|
793
|
+
if isinstance(node, BinaryOp):
|
|
794
|
+
return self._is_constant_expression(node.left) and self._is_constant_expression(node.right)
|
|
795
|
+
if isinstance(node, UnaryOp):
|
|
796
|
+
return self._is_constant_expression(node.operand)
|
|
797
|
+
if isinstance(node, TernaryOp):
|
|
798
|
+
return all(self._is_constant_expression(part)
|
|
799
|
+
for part in (node.condition, node.true_expr, node.false_expr))
|
|
800
|
+
return False
|
|
801
|
+
|
|
802
|
+
# ========================================================================
|
|
803
|
+
# Top-level transformations
|
|
804
|
+
# ========================================================================
|
|
805
|
+
|
|
806
|
+
def _transform_script_declaration(self, decl: Union[IndicatorDecl, StrategyDecl, LibraryDecl]) -> Union[IndicatorDecl, StrategyDecl, LibraryDecl]:
|
|
807
|
+
"""Transform indicator/strategy/library declaration (Rules 2-3, 17, 54)."""
|
|
808
|
+
# Transform kwargs: convert true/false to True/False (Rule 17, 54)
|
|
809
|
+
transformed_kwargs = {}
|
|
810
|
+
for key, value in decl.kwargs.items():
|
|
811
|
+
transformed_kwargs[key] = self._transform_decorator_value(value)
|
|
812
|
+
if self.three_state_bool:
|
|
813
|
+
# Pine v4 and v5: `bool b = na` is a real na (card #99, item 5; the runtime's NA(bool) is False without this)
|
|
814
|
+
transformed_kwargs['na_bool'] = True
|
|
815
|
+
|
|
816
|
+
return type(decl)(title=decl.title, kwargs=transformed_kwargs, line=decl.line,
|
|
817
|
+
positional_count=decl.positional_count, title_double_quoted=decl.title_double_quoted,
|
|
818
|
+
keyword_order=decl.keyword_order)
|
|
819
|
+
|
|
820
|
+
def _transform_decorator_value(self, value: Any) -> Any:
|
|
821
|
+
"""Transform decorator keyword argument values (Rule 17, 54)."""
|
|
822
|
+
if isinstance(value, Literal):
|
|
823
|
+
if value.literal_type == 'bool':
|
|
824
|
+
# Convert true/false to True/False (Rule 17)
|
|
825
|
+
return value.value # Will be True or False in Python
|
|
826
|
+
return value
|
|
827
|
+
elif isinstance(value, Identifier):
|
|
828
|
+
# Handle true/false identifiers
|
|
829
|
+
if value.name == 'true':
|
|
830
|
+
return True
|
|
831
|
+
elif value.name == 'false':
|
|
832
|
+
return False
|
|
833
|
+
return value
|
|
834
|
+
|
|
835
|
+
def _extract_input_declarations(self, script: Script) -> None:
|
|
836
|
+
"""Extract input declarations for main() parameters (Rules 8-9)."""
|
|
837
|
+
for decl in script.declarations:
|
|
838
|
+
if isinstance(decl, InputDecl):
|
|
839
|
+
self.input_declarations.append(decl)
|
|
840
|
+
|
|
841
|
+
def _create_main_parameters(self) -> List[Parameter]:
|
|
842
|
+
"""Create main() function parameters from input declarations (Rule 8)."""
|
|
843
|
+
params = []
|
|
844
|
+
input_names = {input_decl.name for input_decl in self.input_declarations}
|
|
845
|
+
|
|
846
|
+
for input_decl in self.input_declarations:
|
|
847
|
+
args = list(input_decl.args)
|
|
848
|
+
kwargs = dict(input_decl.kwargs)
|
|
849
|
+
|
|
850
|
+
# Convert excess positional args to keyword args.
|
|
851
|
+
# Pine Script allows input.int(defval, title, minval, maxval, ...)
|
|
852
|
+
# but PyneCore requires minval=, maxval=, ... as keyword-only.
|
|
853
|
+
args, positional_keywords = bind_input_positional_args(input_decl.func, args)
|
|
854
|
+
# the positionals bound to names come first, in their order, then the source's keywords
|
|
855
|
+
kwargs = {**positional_keywords, **kwargs}
|
|
856
|
+
kwargs = self._input_keywords_without_input_reads(input_decl, kwargs, input_names)
|
|
857
|
+
|
|
858
|
+
func_call = FunctionCall(
|
|
859
|
+
func=input_decl.func,
|
|
860
|
+
args=args,
|
|
861
|
+
kwargs=kwargs
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
# A source input (input.source, or input() of a price) is a series: Series[float]
|
|
865
|
+
if self._is_source_input(input_decl.func, args, kwargs):
|
|
866
|
+
params.append(Parameter(name=input_decl.name, type_hint='Series[float]', default=func_call,
|
|
867
|
+
line=input_decl.line))
|
|
868
|
+
self.import_resolver.uses_series = True
|
|
869
|
+
continue
|
|
870
|
+
|
|
871
|
+
param = Parameter(
|
|
872
|
+
name=input_decl.name,
|
|
873
|
+
type_hint=self._input_parameter_annotation(input_decl.name, func_call),
|
|
874
|
+
default=func_call,
|
|
875
|
+
line=input_decl.line
|
|
876
|
+
)
|
|
877
|
+
params.append(param)
|
|
878
|
+
|
|
879
|
+
return params
|
|
880
|
+
|
|
881
|
+
@staticmethod
|
|
882
|
+
def _is_source_input(function: str, args: list, kwargs: dict) -> bool:
|
|
883
|
+
if function == 'input.source':
|
|
884
|
+
return True
|
|
885
|
+
if function != 'input':
|
|
886
|
+
return False
|
|
887
|
+
default = args[0] if args else kwargs.get('defval')
|
|
888
|
+
return isinstance(default, Identifier) and default.name in (
|
|
889
|
+
'close', 'open', 'high', 'low', 'volume', 'hl2', 'hlc3', 'ohlc4', 'hlcc4')
|
|
890
|
+
|
|
891
|
+
@staticmethod
|
|
892
|
+
def _input_keywords_without_input_reads(input_decl: InputDecl, keywords: dict, input_names: set) -> dict:
|
|
893
|
+
"""The input's keyword arguments (`keywords`, its positional ones bound by name), for a
|
|
894
|
+
main() parameter default, which the module evaluates before any input exists (card #89). Pine lets two arguments read another
|
|
895
|
+
input; the semantic pass refuses the rest. `active` (an input bool) only greys out the
|
|
896
|
+
field in TradingView, and pynecore never reads it: it is dropped when it reads an input.
|
|
897
|
+
input.source's `defval` (a series float) would make the default follow the other
|
|
898
|
+
input's setting, which a parameter default cannot: refused as unsupported."""
|
|
899
|
+
def reads_input(argument) -> Optional[str]:
|
|
900
|
+
return next((node.name for node in walk(argument)
|
|
901
|
+
if isinstance(node, Identifier) and node.name in input_names), None)
|
|
902
|
+
|
|
903
|
+
kwargs = dict(keywords)
|
|
904
|
+
if 'active' in kwargs and not isinstance(kwargs['active'], Literal):
|
|
905
|
+
# `active` only greys the field out in TradingView: PyneComp keeps a constant one only
|
|
906
|
+
del kwargs['active']
|
|
907
|
+
defval = input_decl.args[0] if input_decl.args else kwargs.get('defval')
|
|
908
|
+
read = reads_input(defval) if input_decl.func == 'input.source' else None
|
|
909
|
+
if read:
|
|
910
|
+
raise UnsupportedFeatureError(
|
|
911
|
+
f"the default of source input '{input_decl.name}' reads the input '{read}'; a default that "
|
|
912
|
+
f"follows another input's setting has no PyneCore equivalent", input_decl.line)
|
|
913
|
+
return kwargs
|
|
914
|
+
|
|
915
|
+
def _input_parameter_annotation(self, name: str, input_call: FunctionCall) -> Optional[str]:
|
|
916
|
+
"""`Series[T]` for an input some scope reads with history (`length[1]`), else none.
|
|
917
|
+
|
|
918
|
+
An input is a main() parameter holding a plain value; the runtime keeps a
|
|
919
|
+
history only for a name annotated as a series. The parameter keeps its name,
|
|
920
|
+
which is the input's settings id (card #90)."""
|
|
921
|
+
if name not in self.names_read_with_history:
|
|
922
|
+
return None
|
|
923
|
+
element_type = self.type_inference.infer_type_hint(input_call)
|
|
924
|
+
return self._series_annotation(name, None if element_type == 'Any' else element_type)
|
|
925
|
+
|
|
926
|
+
# ========================================================================
|
|
927
|
+
# Function transformations
|
|
928
|
+
# ========================================================================
|
|
929
|
+
|
|
930
|
+
@staticmethod
|
|
931
|
+
def _history_reads(tree) -> set:
|
|
932
|
+
"""Names read with history (`source[i]`) anywhere in `tree`, including
|
|
933
|
+
else-if arms, if/switch-expression branches and switch cases (card #44)."""
|
|
934
|
+
return {node.object.name for node in walk(tree)
|
|
935
|
+
if isinstance(node, IndexAccess) and isinstance(node.object, Identifier)}
|
|
936
|
+
|
|
937
|
+
def _main_scope_history_reads(self, script: Script) -> set:
|
|
938
|
+
"""Names the main scope reads with history: its own `x[n]` sites, and a
|
|
939
|
+
function's sites on a name that is not its parameter, which is a global
|
|
940
|
+
(a function local that shadows a global was renamed, card #38)."""
|
|
941
|
+
names = set()
|
|
942
|
+
for item in list(script.declarations) + list(script.body):
|
|
943
|
+
if isinstance(item, FuncDecl):
|
|
944
|
+
names |= self._history_reads(item.body) - {param.name for param in item.params}
|
|
945
|
+
else:
|
|
946
|
+
names |= self._history_reads(item)
|
|
947
|
+
return names
|
|
948
|
+
|
|
949
|
+
def _transform_function(self, func: FuncDecl) -> FuncDecl:
|
|
950
|
+
"""Transform function declaration (Rules 20-22, 46-48)."""
|
|
951
|
+
# Save current function params context
|
|
952
|
+
old_params = self.current_function_params
|
|
953
|
+
self.current_function_params = {}
|
|
954
|
+
# the parameters of this function without a declared type: PyneComp routes their methods at run time
|
|
955
|
+
old_untyped_parameters = getattr(self, '_untyped_parameters', frozenset())
|
|
956
|
+
self._untyped_parameters = frozenset(param.name for param in func.params if not param.type_hint)
|
|
957
|
+
old_shadowed_names = self.current_function_shadowed_names
|
|
958
|
+
self.current_function_shadowed_names = self._function_shadowed_names(func)
|
|
959
|
+
typing = getattr(self, '_condition_typing', None)
|
|
960
|
+
old_typing_scope = getattr(self, '_typing_scope', None)
|
|
961
|
+
self._typing_scope = func
|
|
962
|
+
if typing is not None:
|
|
963
|
+
typing.enter(func)
|
|
964
|
+
|
|
965
|
+
# PRIORITY 1 FIX: Save and set function context
|
|
966
|
+
old_in_function = self.in_user_function
|
|
967
|
+
old_series_vars = self.series_variables
|
|
968
|
+
old_local_vars = self.function_local_vars
|
|
969
|
+
|
|
970
|
+
self.in_user_function = True
|
|
971
|
+
self.series_variables = self._find_series_variables_in_function(
|
|
972
|
+
func.body if isinstance(func.body, list) else [func.body]
|
|
973
|
+
)
|
|
974
|
+
self.function_local_vars = set()
|
|
975
|
+
|
|
976
|
+
old_history_reads = self.names_read_with_history
|
|
977
|
+
self.names_read_with_history = self._history_reads(func.body)
|
|
978
|
+
|
|
979
|
+
# Find which parameters are indexed in the function body
|
|
980
|
+
param_names = {p.name for p in func.params}
|
|
981
|
+
indexed_params = self.names_read_with_history & param_names
|
|
982
|
+
|
|
983
|
+
# Transform parameters
|
|
984
|
+
# For methods, the first parameter has special syntax
|
|
985
|
+
transformed_params = []
|
|
986
|
+
for i, param in enumerate(func.params):
|
|
987
|
+
# Convert Pine generic types to Python types
|
|
988
|
+
if param.type_hint:
|
|
989
|
+
type_hint = convert_pine_generic_to_python(param.type_hint)
|
|
990
|
+
# Wrap in Series[T] if parameter is indexed with [n] history access.
|
|
991
|
+
# The runtime SeriesTransformer will create SeriesImpl and register it.
|
|
992
|
+
if param.name in indexed_params or param.is_series:
|
|
993
|
+
type_hint = f'Series[{type_hint}]'
|
|
994
|
+
else:
|
|
995
|
+
# If parameter is indexed and has no type hint, add bare Series
|
|
996
|
+
type_hint = 'Series' if param.name in indexed_params else None
|
|
997
|
+
|
|
998
|
+
# Track parameter types for method call transformation
|
|
999
|
+
if type_hint:
|
|
1000
|
+
self.current_function_params[param.name] = type_hint
|
|
1001
|
+
|
|
1002
|
+
default_value = self._transform_expression(param.default) if param.default else None
|
|
1003
|
+
if param.type_hint == 'bool' and isinstance(param.default, NaLiteral):
|
|
1004
|
+
default_value = Literal(value=False, literal_type='bool') # `bool b = na` is `b: bool = False`
|
|
1005
|
+
if default_value is None and any(earlier.default for earlier in func.params[:i]):
|
|
1006
|
+
# Python needs a default after a defaulted parameter: PyneComp gives it na
|
|
1007
|
+
default_value = Identifier(name='na')
|
|
1008
|
+
|
|
1009
|
+
transformed_params.append(Parameter(
|
|
1010
|
+
name=param.name,
|
|
1011
|
+
type_hint=type_hint,
|
|
1012
|
+
default=default_value
|
|
1013
|
+
))
|
|
1014
|
+
|
|
1015
|
+
# Transform body
|
|
1016
|
+
if isinstance(func.body, Expression):
|
|
1017
|
+
# Single-line arrow function (Rule 21)
|
|
1018
|
+
transformed_body = ReturnStatement(expr=self._transform_expression(func.body))
|
|
1019
|
+
else:
|
|
1020
|
+
# Multi-line function (Rule 22)
|
|
1021
|
+
transformed_body = [self._transform_statement(stmt) for stmt in func.body]
|
|
1022
|
+
# The value of a Pine function is the value of its last statement (Rule 20);
|
|
1023
|
+
# this also flattens statement lists and drops None statements
|
|
1024
|
+
transformed_body = self._return_tail_value(transformed_body, untaken_is_false=func.value_is_bool)
|
|
1025
|
+
|
|
1026
|
+
# Restore previous params context
|
|
1027
|
+
self.current_function_params = old_params
|
|
1028
|
+
self._untyped_parameters = old_untyped_parameters
|
|
1029
|
+
self.current_function_shadowed_names = old_shadowed_names
|
|
1030
|
+
|
|
1031
|
+
# PRIORITY 1 FIX: Restore function context
|
|
1032
|
+
self._typing_scope = old_typing_scope
|
|
1033
|
+
if typing is not None:
|
|
1034
|
+
typing.enter(old_typing_scope)
|
|
1035
|
+
self.in_user_function = old_in_function
|
|
1036
|
+
self.series_variables = old_series_vars
|
|
1037
|
+
self.function_local_vars = old_local_vars
|
|
1038
|
+
self.names_read_with_history = old_history_reads
|
|
1039
|
+
|
|
1040
|
+
# Track method functions, with their `this` types, for call transformation
|
|
1041
|
+
if func.is_method:
|
|
1042
|
+
this_hint = func.params[0].type_hint if func.params else None
|
|
1043
|
+
this_type = convert_pine_generic_to_python(this_hint) if this_hint else None
|
|
1044
|
+
self.method_functions.setdefault(func.name, set()).add(this_type)
|
|
1045
|
+
|
|
1046
|
+
# Sanitize function name if it clashes with Python reserved words
|
|
1047
|
+
from .pine_builtins import sanitize_identifier
|
|
1048
|
+
sanitized_name = sanitize_identifier(func.name)
|
|
1049
|
+
if sanitized_name != func.name:
|
|
1050
|
+
self._renamed_functions[func.name] = sanitized_name
|
|
1051
|
+
|
|
1052
|
+
return FuncDecl(
|
|
1053
|
+
name=sanitized_name,
|
|
1054
|
+
params=transformed_params,
|
|
1055
|
+
body=transformed_body,
|
|
1056
|
+
is_method=func.is_method,
|
|
1057
|
+
is_export=func.is_export, # codegen emits `@export` for it (card #111)
|
|
1058
|
+
indexed_params=indexed_params,
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
def _return_tail_value(self, body: List[Statement], is_function_body: bool = True,
|
|
1062
|
+
untaken_is_false: bool = False) -> List[Statement]:
|
|
1063
|
+
"""Return a copy of a function body (or of one arm of its tail if) that returns its value.
|
|
1064
|
+
|
|
1065
|
+
An expression tail becomes ``return expr``; an assignment tail is followed by
|
|
1066
|
+
``return target``. An if tail is rewritten arm by arm (if, every else-if, else),
|
|
1067
|
+
recursively, so the arm that runs returns its own value. An arm that yields no
|
|
1068
|
+
value (an untaken if without else, a loop) falls through to one ``return na``
|
|
1069
|
+
after the function's tail if, or ``return False`` when `untaken_is_false`
|
|
1070
|
+
(Pine v6: the function's value is of type bool, card #56). Pine has no early return, so returning from inside
|
|
1071
|
+
the LAST statement skips nothing that would have run after it.
|
|
1072
|
+
"""
|
|
1073
|
+
statements = self._flatten_statements(body)
|
|
1074
|
+
if not statements:
|
|
1075
|
+
return statements
|
|
1076
|
+
last = statements[-1]
|
|
1077
|
+
if isinstance(last, ExpressionStatement):
|
|
1078
|
+
return statements[:-1] + [self._keep_source_line(last, ReturnStatement(expr=last.expr))]
|
|
1079
|
+
if isinstance(last, Assignment):
|
|
1080
|
+
return statements + [ReturnStatement(expr=self._assigned_value(last.target))]
|
|
1081
|
+
if not isinstance(last, IfStatement):
|
|
1082
|
+
return statements
|
|
1083
|
+
returning_if = self._keep_source_line(last, IfStatement(
|
|
1084
|
+
condition=last.condition,
|
|
1085
|
+
body=self._return_tail_value(last.body, is_function_body=False),
|
|
1086
|
+
elseifs=[(condition, self._return_tail_value(arm, is_function_body=False))
|
|
1087
|
+
for condition, arm in last.elseifs],
|
|
1088
|
+
else_body=(self._return_tail_value(last.else_body, is_function_body=False)
|
|
1089
|
+
if last.else_body else None),
|
|
1090
|
+
))
|
|
1091
|
+
# No trailing `return na` when every arm returns: it could never run
|
|
1092
|
+
if not is_function_body or self._always_returns([returning_if]):
|
|
1093
|
+
return statements[:-1] + [returning_if]
|
|
1094
|
+
if untaken_is_false:
|
|
1095
|
+
return statements[:-1] + [returning_if, ReturnStatement(expr=Literal(value=False, literal_type='bool'))]
|
|
1096
|
+
# Pine's value for an untaken if, or for an arm without a value, is na
|
|
1097
|
+
self.import_resolver.lib_modules.add('na')
|
|
1098
|
+
return statements[:-1] + [returning_if, ReturnStatement(expr=Identifier(name='na'))]
|
|
1099
|
+
|
|
1100
|
+
@staticmethod
|
|
1101
|
+
def _assigned_value(target) -> Expression:
|
|
1102
|
+
"""The value an assignment leaves behind: the variable, or the tuple of destructured names."""
|
|
1103
|
+
if isinstance(target, TupleDestructure):
|
|
1104
|
+
return ArrayLiteral(elements=[Identifier(name=name) for name in target.names])
|
|
1105
|
+
return Identifier(name=target) if isinstance(target, str) else target
|
|
1106
|
+
|
|
1107
|
+
@staticmethod
|
|
1108
|
+
def _always_returns(statements: List[Statement]) -> bool:
|
|
1109
|
+
"""True when every path through the statements ends in a return."""
|
|
1110
|
+
if not statements:
|
|
1111
|
+
return False
|
|
1112
|
+
last = statements[-1]
|
|
1113
|
+
if isinstance(last, ReturnStatement):
|
|
1114
|
+
return True
|
|
1115
|
+
if isinstance(last, IfStatement) and last.else_body:
|
|
1116
|
+
arms = [last.body] + [arm for _, arm in last.elseifs] + [last.else_body]
|
|
1117
|
+
return all(Transformer._always_returns(arm) for arm in arms)
|
|
1118
|
+
return False
|
|
1119
|
+
|
|
1120
|
+
@staticmethod
|
|
1121
|
+
def _flatten_statements(statements: List[Statement]) -> List[Statement]:
|
|
1122
|
+
"""Branch bodies keep statement lists (a split tuple, a loop used as a value) and dropped
|
|
1123
|
+
(None) statements; flatten them."""
|
|
1124
|
+
flat = []
|
|
1125
|
+
for statement in statements:
|
|
1126
|
+
if statement is None:
|
|
1127
|
+
continue
|
|
1128
|
+
if isinstance(statement, list):
|
|
1129
|
+
flat.extend(Transformer._flatten_statements(statement))
|
|
1130
|
+
else:
|
|
1131
|
+
flat.append(statement)
|
|
1132
|
+
return flat
|
|
1133
|
+
|
|
1134
|
+
# ========================================================================
|
|
1135
|
+
# Statement transformations
|
|
1136
|
+
# ========================================================================
|
|
1137
|
+
|
|
1138
|
+
def _transform_statement(self, stmt: Statement) -> Union[Statement, List[Statement], None]:
|
|
1139
|
+
"""Transform statement."""
|
|
1140
|
+
if isinstance(stmt, Assignment):
|
|
1141
|
+
result = self._transform_assignment(stmt)
|
|
1142
|
+
elif isinstance(stmt, Reassignment):
|
|
1143
|
+
result = self._transform_reassignment(stmt)
|
|
1144
|
+
elif isinstance(stmt, (VarDecl, VaripDecl)):
|
|
1145
|
+
result = self._transform_var_declaration(stmt)
|
|
1146
|
+
elif isinstance(stmt, IfStatement):
|
|
1147
|
+
result = self._transform_if_statement(stmt)
|
|
1148
|
+
elif isinstance(stmt, ForLoop):
|
|
1149
|
+
result = self._transform_for_loop(stmt)
|
|
1150
|
+
elif isinstance(stmt, ForInLoop):
|
|
1151
|
+
result = self._transform_for_in_loop(stmt)
|
|
1152
|
+
elif isinstance(stmt, WhileLoop):
|
|
1153
|
+
result = self._transform_while_loop(stmt)
|
|
1154
|
+
elif isinstance(stmt, SwitchStatement):
|
|
1155
|
+
result = self._transform_switch_statement(stmt)
|
|
1156
|
+
elif isinstance(stmt, ExpressionStatement):
|
|
1157
|
+
# Skip stray declaration calls (indicator/strategy/library) in body
|
|
1158
|
+
if isinstance(stmt.expr, FunctionCall) and isinstance(stmt.expr.func, str):
|
|
1159
|
+
if stmt.expr.func in ('indicator', 'strategy', 'library'):
|
|
1160
|
+
return None
|
|
1161
|
+
result = ExpressionStatement(expr=self._transform_expression(stmt.expr))
|
|
1162
|
+
elif isinstance(stmt, (BreakStatement, ContinueStatement)):
|
|
1163
|
+
result = stmt
|
|
1164
|
+
else:
|
|
1165
|
+
result = stmt
|
|
1166
|
+
return self._keep_source_line(stmt, result)
|
|
1167
|
+
|
|
1168
|
+
@staticmethod
|
|
1169
|
+
def _keep_source_line(source: Statement, result):
|
|
1170
|
+
"""A statement built for a nested Pine statement remembers its Pine line in
|
|
1171
|
+
`source_line`, so a layout printer can place blank lines where the source
|
|
1172
|
+
has them. `line` stays unset: refusals keep reporting the enclosing
|
|
1173
|
+
top-level line (card #40). Only the first statement of an expansion takes it."""
|
|
1174
|
+
line = getattr(source, 'line', 0) or getattr(source, 'source_line', 0)
|
|
1175
|
+
first = result[0] if isinstance(result, list) and result else result
|
|
1176
|
+
if line and first is not None and not isinstance(first, list) and not getattr(first, 'line', 0):
|
|
1177
|
+
try:
|
|
1178
|
+
first.source_line = line
|
|
1179
|
+
except AttributeError:
|
|
1180
|
+
pass
|
|
1181
|
+
return result
|
|
1182
|
+
|
|
1183
|
+
def _transform_assignment(self, stmt: Assignment) -> Union[Assignment, List[Statement]]:
|
|
1184
|
+
"""Transform assignment (Rules 5-7)."""
|
|
1185
|
+
# `[a, b] = g()` stays one destructure, as PyneComp writes it; a name this scope reads with history is
|
|
1186
|
+
# declared first, bare: `p: Series` (probe tup1)
|
|
1187
|
+
if isinstance(stmt.target, TupleDestructure) and not getattr(self, '_declaring_tuple', False):
|
|
1188
|
+
series_names = [name for name in stmt.target.names if name in self.names_read_with_history]
|
|
1189
|
+
if series_names:
|
|
1190
|
+
self.import_resolver.uses_series = True
|
|
1191
|
+
self._declaring_tuple = True
|
|
1192
|
+
try:
|
|
1193
|
+
rest = self._transform_assignment(stmt)
|
|
1194
|
+
finally:
|
|
1195
|
+
self._declaring_tuple = False
|
|
1196
|
+
declarations = [RawCode(code=f'{name}: Series') for name in series_names]
|
|
1197
|
+
return declarations + (rest if isinstance(rest, list) else [rest])
|
|
1198
|
+
|
|
1199
|
+
# PRIORITY 1 FIX: Track first assignment to Series variables in functions
|
|
1200
|
+
is_first_assignment_to_series = False
|
|
1201
|
+
if self.in_user_function and isinstance(stmt.target, str):
|
|
1202
|
+
if stmt.target in self.series_variables and stmt.target not in self.function_local_vars:
|
|
1203
|
+
is_first_assignment_to_series = True
|
|
1204
|
+
self.function_local_vars.add(stmt.target)
|
|
1205
|
+
|
|
1206
|
+
# Determine type annotation first (we need it for na value transformation)
|
|
1207
|
+
type_hint = None
|
|
1208
|
+
converted_type = None
|
|
1209
|
+
if isinstance(stmt.target, str):
|
|
1210
|
+
symbol = self.symbol_table.lookup(stmt.target)
|
|
1211
|
+
if self.in_user_function and stmt.target in self.current_function_shadowed_names:
|
|
1212
|
+
symbol = None # a function's own local, not the global it hides
|
|
1213
|
+
# CRITICAL: In Pine Script, series is the default type qualifier
|
|
1214
|
+
if hasattr(stmt, 'type_hint') and stmt.type_hint:
|
|
1215
|
+
# Use the explicit type hint from parser (e.g., map<int, float> mapExample = na)
|
|
1216
|
+
converted_type = convert_pine_generic_to_python(stmt.type_hint)
|
|
1217
|
+
# If variable is used with historical reference [], wrap in Series[type]
|
|
1218
|
+
if symbol and symbol.is_indexed:
|
|
1219
|
+
type_hint = self._series_annotation(stmt.target, converted_type)
|
|
1220
|
+
else:
|
|
1221
|
+
type_hint = converted_type
|
|
1222
|
+
elif symbol:
|
|
1223
|
+
# No explicit type in Pine source — only infer for na value transformation
|
|
1224
|
+
# and var/varip persistence (the same rule as a var declaration)
|
|
1225
|
+
if symbol.kind in (VariableKind.VAR, VariableKind.VARIP):
|
|
1226
|
+
base_type = symbol.type_hint or self.type_inference.infer_type_hint(stmt.value)
|
|
1227
|
+
converted_type = base_type
|
|
1228
|
+
type_hint = self._series_annotation(
|
|
1229
|
+
stmt.target, self._series_element_type(base_type, stmt.value), persistent=True)
|
|
1230
|
+
elif isinstance(stmt.value, NaLiteral):
|
|
1231
|
+
# Need type for na() conversion even without explicit annotation
|
|
1232
|
+
base_type = symbol.type_hint or self.type_inference.infer_type_hint(stmt.value)
|
|
1233
|
+
converted_type = base_type
|
|
1234
|
+
if symbol.is_indexed:
|
|
1235
|
+
type_hint = self._series_annotation(
|
|
1236
|
+
stmt.target, self._series_element_type(base_type, stmt.value))
|
|
1237
|
+
elif isinstance(stmt.target, str) and stmt.target in getattr(self, '_reassignment_vars', set()) and symbol.is_indexed:
|
|
1238
|
+
# Variable uses both := (reassignment) and [] (historical reference)
|
|
1239
|
+
# WITHOUT 'var' keyword - this is a Series in Pine Script semantics
|
|
1240
|
+
base_type = self._series_element_type(
|
|
1241
|
+
symbol.type_hint or self.type_inference.infer_type_hint(stmt.value), stmt.value)
|
|
1242
|
+
converted_type = base_type
|
|
1243
|
+
type_hint = self._series_annotation(stmt.target, base_type)
|
|
1244
|
+
elif isinstance(stmt.target, str) and symbol.is_indexed:
|
|
1245
|
+
# Variable is used with [] - in Pine Script, series is the default type qualifier
|
|
1246
|
+
base_type = self._series_element_type(
|
|
1247
|
+
symbol.type_hint or self.type_inference.infer_type_hint(stmt.value), stmt.value)
|
|
1248
|
+
converted_type = base_type
|
|
1249
|
+
type_hint = self._series_annotation(stmt.target, base_type)
|
|
1250
|
+
# Otherwise: no type annotation for body assignments without explicit Pine types
|
|
1251
|
+
|
|
1252
|
+
# Transform value - special case for na with type hint
|
|
1253
|
+
if isinstance(stmt.value, NaLiteral) and converted_type:
|
|
1254
|
+
value = self._na_initialiser(converted_type)
|
|
1255
|
+
elif getattr(stmt, 'type_hint', None) == 'bool' and not getattr(stmt, 'is_reassignment', False):
|
|
1256
|
+
value = self._condition(stmt.value) # `bool b = ta.change(time('D'))` is cast (PyneComp)
|
|
1257
|
+
else:
|
|
1258
|
+
value = self._transform_expression(stmt.value)
|
|
1259
|
+
|
|
1260
|
+
# Suppress 'Any' type annotations - omit annotation when type is unknown
|
|
1261
|
+
if type_hint == 'Any':
|
|
1262
|
+
type_hint = None
|
|
1263
|
+
|
|
1264
|
+
# PRIORITY 1 FIX: For Series variables in functions, add Series type hint
|
|
1265
|
+
if is_first_assignment_to_series:
|
|
1266
|
+
base_type = self._series_element_type(
|
|
1267
|
+
converted_type or self.type_inference.infer_type_hint(stmt.value), stmt.value)
|
|
1268
|
+
type_hint = self._series_annotation(stmt.target, base_type)
|
|
1269
|
+
|
|
1270
|
+
# Record this local's type in the current-function scope map so later
|
|
1271
|
+
# method calls on it resolve type-aware, e.g. `thisT = m.transpose()`
|
|
1272
|
+
# (thisT: Matrix[int]) then `thisT.sort(...)` → matrix.sort(...). Function
|
|
1273
|
+
# locals are not in the global symbol table, so this is the only place
|
|
1274
|
+
# their type is known; the map is per-function (saved/reset around each
|
|
1275
|
+
# function), and body statements transform in source order so the type is
|
|
1276
|
+
# in place before the call. Only concrete types are recorded.
|
|
1277
|
+
if self.in_user_function and isinstance(stmt.target, str):
|
|
1278
|
+
# Prefer the concrete converted type; otherwise infer from the
|
|
1279
|
+
# TRANSFORMED value (e.g. m.transpose() is now matrix.transpose(m),
|
|
1280
|
+
# whose return type the inference knows), not the pre-transform AST.
|
|
1281
|
+
local_type = converted_type or self.type_inference.infer_type_hint(value)
|
|
1282
|
+
if (not local_type or local_type == 'Any') and isinstance(stmt.value, FunctionCall) \
|
|
1283
|
+
and isinstance(stmt.value.func, str) and stmt.value.func.endswith('.new') \
|
|
1284
|
+
and stmt.value.func[:-4] in self.symbol_table.udt_field_types:
|
|
1285
|
+
local_type = stmt.value.func[:-4] # `t = U.new(...)`: a U
|
|
1286
|
+
if local_type and local_type != 'Any':
|
|
1287
|
+
self.current_function_params[stmt.target] = local_type
|
|
1288
|
+
|
|
1289
|
+
return Assignment(target=stmt.target, value=value, type_hint=type_hint)
|
|
1290
|
+
|
|
1291
|
+
def _series_element_type(self, inferred_type: Optional[str], value: Expression) -> Optional[str]:
|
|
1292
|
+
"""The element type of a Series annotation: the inferred type when it is
|
|
1293
|
+
known, else Color for a colour value (card #32), else None. A guessed
|
|
1294
|
+
float breaks a colour or a string held in an untyped variable (card #48)."""
|
|
1295
|
+
if inferred_type and inferred_type != 'Any':
|
|
1296
|
+
return inferred_type
|
|
1297
|
+
return 'Color' if self.type_inference.is_colour_value(value) else None
|
|
1298
|
+
|
|
1299
|
+
def _series_annotation(self, name: str, element_type: Optional[str],
|
|
1300
|
+
persistent: bool = False) -> Optional[str]:
|
|
1301
|
+
"""The annotation of a name that keeps a history (card #48).
|
|
1302
|
+
|
|
1303
|
+
`Series[T]`, or for a var `PersistentSeries[T]`, which the runtime splits
|
|
1304
|
+
into `Persistent[T]` + `Series[T]` so that `x[n]` works on any var. The
|
|
1305
|
+
runtime drops a `Series[T]` half that is never indexed. An element type
|
|
1306
|
+
the inference cannot name (None) gives the bare form: the runtime types
|
|
1307
|
+
it from the values assigned, as for a typeless na (card #10). The
|
|
1308
|
+
runtime never drops a bare Series half, so the bare form is emitted only
|
|
1309
|
+
for a name this scope reads with history; any other name gets no Series
|
|
1310
|
+
half: a var is a bare `Persistent`, anything else has no annotation."""
|
|
1311
|
+
if not element_type and name not in self.names_read_with_history:
|
|
1312
|
+
return 'Persistent' if persistent else None
|
|
1313
|
+
if persistent:
|
|
1314
|
+
kind = 'PersistentSeries'
|
|
1315
|
+
self.import_resolver.uses_persistent_series = True
|
|
1316
|
+
else:
|
|
1317
|
+
kind = 'Series'
|
|
1318
|
+
self.import_resolver.uses_series = True
|
|
1319
|
+
return f"{kind}[{element_type}]" if element_type else kind
|
|
1320
|
+
|
|
1321
|
+
def _transform_reassignment(self, stmt: Reassignment) -> Assignment:
|
|
1322
|
+
"""
|
|
1323
|
+
Transform reassignment := to = (Rule 11).
|
|
1324
|
+
"""
|
|
1325
|
+
# Default behavior: regular assignment
|
|
1326
|
+
# PRIORITY 1 FIX: Series type hint will be added during assignment transformation
|
|
1327
|
+
return Assignment(
|
|
1328
|
+
target=stmt.target,
|
|
1329
|
+
value=self._transform_expression(stmt.value)
|
|
1330
|
+
)
|
|
1331
|
+
|
|
1332
|
+
def _transform_var_declaration(self, decl: Union[VarDecl, VaripDecl]) -> Assignment:
|
|
1333
|
+
"""Transform var/varip declaration to a PersistentSeries assignment (Rule 5-6).
|
|
1334
|
+
|
|
1335
|
+
Every type, scalar or not, keeps a Series half, so `x[n]` reads the
|
|
1336
|
+
value (for an array, map, matrix, UDT or drawing: the reference) from n
|
|
1337
|
+
bars ago, as in Pine (card #48). The exception is a var of unknown type
|
|
1338
|
+
that no scope reads with history: a bare `Persistent` (`_series_annotation`).
|
|
1339
|
+
"""
|
|
1340
|
+
base_type = decl.type_hint or self.type_inference.infer_type_hint(decl.value)
|
|
1341
|
+
# Convert generic types properly
|
|
1342
|
+
converted_type = convert_pine_generic_to_python(base_type)
|
|
1343
|
+
|
|
1344
|
+
# Special case: if value is na, convert to na(Type) function call
|
|
1345
|
+
value = decl.value
|
|
1346
|
+
if isinstance(value, NaLiteral):
|
|
1347
|
+
value = self._na_initialiser(converted_type)
|
|
1348
|
+
else:
|
|
1349
|
+
value = self._transform_expression(value)
|
|
1350
|
+
|
|
1351
|
+
type_hint = self._series_annotation(
|
|
1352
|
+
decl.name, self._series_element_type(converted_type, decl.value), persistent=True)
|
|
1353
|
+
if isinstance(decl, VaripDecl) and type_hint:
|
|
1354
|
+
# varip persists across a bar's intrabar updates too: pynecore's IBPersistent (PyneComp)
|
|
1355
|
+
type_hint = 'IB' + type_hint
|
|
1356
|
+
|
|
1357
|
+
# a var local of a function types its method calls, as an assignment's local does
|
|
1358
|
+
if self.in_user_function and converted_type and converted_type != 'Any':
|
|
1359
|
+
self.current_function_params[decl.name] = converted_type
|
|
1360
|
+
|
|
1361
|
+
return Assignment(
|
|
1362
|
+
target=decl.name,
|
|
1363
|
+
value=value,
|
|
1364
|
+
type_hint=type_hint
|
|
1365
|
+
)
|
|
1366
|
+
|
|
1367
|
+
_NUMERIC_TYPES = frozenset({'int', 'float'})
|
|
1368
|
+
|
|
1369
|
+
def _condition(self, pine_expr: Expression, header: bool = False, negated: bool = False,
|
|
1370
|
+
ternary: Optional[TernaryOp] = None) -> Expression:
|
|
1371
|
+
"""A Pine expression read as a condition: a number there is cast_bool(...) (PyneComp, see
|
|
1372
|
+
condition_typing.py). `header`: an if/while condition; `negated`: the operand of `not`."""
|
|
1373
|
+
transformed = self._transform_expression(pine_expr)
|
|
1374
|
+
typing = getattr(self, '_condition_typing', None)
|
|
1375
|
+
if typing is None:
|
|
1376
|
+
numeric = self.type_inference.infer_type_hint(pine_expr) in self._NUMERIC_TYPES
|
|
1377
|
+
else:
|
|
1378
|
+
colours = ternary is not None and (self.type_inference.is_colour_value(ternary.true_expr)
|
|
1379
|
+
or self.type_inference.is_colour_value(ternary.false_expr))
|
|
1380
|
+
numeric = (typing.is_number(pine_expr, header, colour_ternary=colours, ternary=ternary is not None)
|
|
1381
|
+
and (not negated or typing.is_pivot(pine_expr)))
|
|
1382
|
+
if numeric:
|
|
1383
|
+
self.import_resolver.uses_cast_bool = True
|
|
1384
|
+
return FunctionCall(func='cast_bool', args=[transformed], kwargs={})
|
|
1385
|
+
return transformed
|
|
1386
|
+
|
|
1387
|
+
def _transform_if_statement(self, stmt: IfStatement) -> IfStatement:
|
|
1388
|
+
"""Transform if statement (Rule 15)."""
|
|
1389
|
+
condition = self._condition(stmt.condition, header=True)
|
|
1390
|
+
body = [self._transform_statement(s) for s in stmt.body]
|
|
1391
|
+
|
|
1392
|
+
elseifs = []
|
|
1393
|
+
for elif_cond, elif_body in stmt.elseifs:
|
|
1394
|
+
transformed_cond = self._condition(elif_cond, header=True)
|
|
1395
|
+
transformed_body = [self._transform_statement(s) for s in elif_body]
|
|
1396
|
+
elseifs.append((transformed_cond, transformed_body))
|
|
1397
|
+
|
|
1398
|
+
else_body = None
|
|
1399
|
+
if stmt.else_body:
|
|
1400
|
+
else_body = [self._transform_statement(s) for s in stmt.else_body]
|
|
1401
|
+
|
|
1402
|
+
return IfStatement(
|
|
1403
|
+
condition=condition,
|
|
1404
|
+
body=body,
|
|
1405
|
+
elseifs=elseifs,
|
|
1406
|
+
else_body=else_body
|
|
1407
|
+
)
|
|
1408
|
+
|
|
1409
|
+
def _transform_for_loop(self, stmt: ForLoop) -> ForLoop:
|
|
1410
|
+
"""Transform for loop to range() (Rules 12-13)."""
|
|
1411
|
+
from_val = self._transform_expression(stmt.from_val)
|
|
1412
|
+
to_val = self._transform_expression(stmt.to_val)
|
|
1413
|
+
step = self._transform_expression(stmt.step) if stmt.step else None
|
|
1414
|
+
if step is None and self.pine_version < 6 and not stmt.is_assigned_value:
|
|
1415
|
+
# PyneComp spells out a Pine v4/v5 loop's default step (635 loops of the reference), not
|
|
1416
|
+
# for a loop given as the value of an assignment (probe tmp/probes/lv5.pine)
|
|
1417
|
+
step = Literal(value=1, literal_type='int')
|
|
1418
|
+
body = [self._transform_statement(s) for s in stmt.body]
|
|
1419
|
+
# a loop whose end may change in its body reads it before every pass: pine_loop (for_loop_end_bound.py)
|
|
1420
|
+
return ForLoop(
|
|
1421
|
+
var=stmt.var,
|
|
1422
|
+
from_val=from_val,
|
|
1423
|
+
to_val=to_val,
|
|
1424
|
+
step=step,
|
|
1425
|
+
body=body,
|
|
1426
|
+
counter_name=stmt.counter_name,
|
|
1427
|
+
counter_helper=stmt.counter_helper,
|
|
1428
|
+
range_helper=self._range_helper_name()
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
def _range_helper_name(self) -> str:
|
|
1432
|
+
"""The name `pine_range` is imported under: its own spelling unless the script uses that name
|
|
1433
|
+
(`pine_range = 2`), then a fresh one, as for `pine_loop` (card #94)."""
|
|
1434
|
+
if self._range_helper is None:
|
|
1435
|
+
if self._used_names is None:
|
|
1436
|
+
self._used_names = collect_names(self._names_source)
|
|
1437
|
+
self._range_helper = (fresh_name(RANGE_HELPER, self._used_names)
|
|
1438
|
+
if RANGE_HELPER in self._used_names else RANGE_HELPER)
|
|
1439
|
+
self.import_resolver.pine_range_name = self._range_helper
|
|
1440
|
+
return self._range_helper
|
|
1441
|
+
|
|
1442
|
+
def _transform_for_in_loop(self, stmt: ForInLoop) -> Union[ForInLoop, List[Statement]]:
|
|
1443
|
+
"""Transform for-in loop (Rule 14)."""
|
|
1444
|
+
iterable = self._transform_expression(stmt.iterable)
|
|
1445
|
+
body = [self._transform_statement(s) for s in stmt.body]
|
|
1446
|
+
if len(stmt.vars) > 1:
|
|
1447
|
+
return self._key_value_loop(stmt, iterable, body)
|
|
1448
|
+
return ForInLoop(
|
|
1449
|
+
vars=stmt.vars,
|
|
1450
|
+
iterable=iterable,
|
|
1451
|
+
body=body
|
|
1452
|
+
)
|
|
1453
|
+
|
|
1454
|
+
def _key_value_loop(self, stmt: ForInLoop, iterable: Expression,
|
|
1455
|
+
body: List[Statement]) -> Union[ForInLoop, List[Statement]]:
|
|
1456
|
+
"""`for [k, v] in x` (card #17): an array or a matrix gives (index, element)
|
|
1457
|
+
through enumerate(); a map gives (key, value) through `.items()`, as PyneComp
|
|
1458
|
+
writes it; a value of unknown type is read as an array."""
|
|
1459
|
+
iterable_type = self._iterable_type(stmt.iterable)
|
|
1460
|
+
if iterable_type and not iterable_type.startswith(MAP_TYPE_PREFIXES):
|
|
1461
|
+
return ForInLoop(vars=stmt.vars, iterable=FunctionCall(func='enumerate', args=[iterable], kwargs={}),
|
|
1462
|
+
body=body)
|
|
1463
|
+
if iterable_type: # a map: `for k, v in m.items():` (PyneComp)
|
|
1464
|
+
return ForInLoop(vars=stmt.vars, iterable=MethodCall(object=iterable, method='items', args=[], kwargs={}),
|
|
1465
|
+
body=body)
|
|
1466
|
+
# a type PyneComp does not know: an array's (index, element) (Trendoscope/Zigzag `_indicatorNames`)
|
|
1467
|
+
return ForInLoop(vars=stmt.vars, iterable=FunctionCall(func='enumerate', args=[iterable], kwargs={}), body=body)
|
|
1468
|
+
|
|
1469
|
+
def _iterable_type(self, pine_iterable: Expression) -> Optional[str]:
|
|
1470
|
+
"""The static type of a for-in iterable, or None. A name or field path has
|
|
1471
|
+
its declared type; `m.keys()`/`m.values()` of a typed map is an array; any
|
|
1472
|
+
other expression counts only when the inference gives a collection
|
|
1473
|
+
(`array.from(...)`, `map.new<K, V>()`)."""
|
|
1474
|
+
if receiver_path(pine_iterable):
|
|
1475
|
+
return self._receiver_type(pine_iterable)
|
|
1476
|
+
if isinstance(pine_iterable, FunctionCall) and isinstance(pine_iterable.func, str):
|
|
1477
|
+
receiver, _, member = pine_iterable.func.rpartition('.')
|
|
1478
|
+
if member in ('keys', 'values') and receiver \
|
|
1479
|
+
and (self._receiver_type(receiver) or '').startswith(MAP_TYPE_PREFIXES):
|
|
1480
|
+
return 'list[Any]'
|
|
1481
|
+
inferred = self.type_inference.declared_type_name(self.type_inference.infer_type_hint(pine_iterable))
|
|
1482
|
+
return inferred if inferred and inferred.startswith(COLLECTION_TYPE_PREFIXES) else None
|
|
1483
|
+
|
|
1484
|
+
def _transform_while_loop(self, stmt: WhileLoop) -> WhileLoop:
|
|
1485
|
+
"""Transform while loop."""
|
|
1486
|
+
condition = self._condition(stmt.condition, header=True)
|
|
1487
|
+
body = [self._transform_statement(s) for s in stmt.body]
|
|
1488
|
+
return WhileLoop(condition=condition, body=body)
|
|
1489
|
+
|
|
1490
|
+
def _transform_switch_statement(self, stmt: SwitchStatement) -> Union[IfStatement, List[Statement], None]:
|
|
1491
|
+
"""Transform switch to if/elif/else chain (Rules 41-42)."""
|
|
1492
|
+
if not stmt.cases:
|
|
1493
|
+
# Default-only switch: the default always runs (card #11); a
|
|
1494
|
+
# subject is still evaluated, as Pine evaluates it
|
|
1495
|
+
subject = [ExpressionStatement(expr=self._transform_expression(stmt.expr))] if stmt.expr else []
|
|
1496
|
+
default_body = [self._transform_statement(s) for s in (stmt.default or [])]
|
|
1497
|
+
return self._flatten_statements(subject + default_body) or None
|
|
1498
|
+
|
|
1499
|
+
# Every case condition first, the first one binding the subject (card #43)
|
|
1500
|
+
first_condition, *elif_conditions = self._switch_case_conditions(stmt.expr, stmt.cases)
|
|
1501
|
+
|
|
1502
|
+
transformed_body = [self._transform_statement(s) for s in stmt.cases[0][1]]
|
|
1503
|
+
elseifs = [(condition, [self._transform_statement(s) for s in case_body])
|
|
1504
|
+
for condition, (_, case_body) in zip(elif_conditions, stmt.cases[1:])]
|
|
1505
|
+
|
|
1506
|
+
# Default case becomes else
|
|
1507
|
+
else_body = None
|
|
1508
|
+
if stmt.default:
|
|
1509
|
+
else_body = [self._transform_statement(s) for s in stmt.default]
|
|
1510
|
+
|
|
1511
|
+
return IfStatement(
|
|
1512
|
+
condition=first_condition,
|
|
1513
|
+
body=transformed_body,
|
|
1514
|
+
elseifs=elseifs,
|
|
1515
|
+
else_body=else_body
|
|
1516
|
+
)
|
|
1517
|
+
|
|
1518
|
+
def _switch_case_conditions(self, subject: Optional[Expression], cases) -> List[Expression]:
|
|
1519
|
+
"""The condition of each case, in case order.
|
|
1520
|
+
|
|
1521
|
+
With a subject, the FIRST condition binds it, `(s := subject) == c1`, and
|
|
1522
|
+
the others read it, `s == c2`: the subject runs once, inside the first case
|
|
1523
|
+
test, which runs exactly when the switch does (card #43). A plain name or a
|
|
1524
|
+
literal is read again as is. Without a subject a case is its own condition.
|
|
1525
|
+
Every read is its own node.
|
|
1526
|
+
|
|
1527
|
+
Statement-position twin: `block_value_lowering._BlockValueLowering.switch_into`
|
|
1528
|
+
binds the subject of a switch lowered to statements with an assignment
|
|
1529
|
+
instead; a change to how one binds the subject belongs in the other too.
|
|
1530
|
+
"""
|
|
1531
|
+
if subject is None:
|
|
1532
|
+
return [self._transform_expression(case) for case, _ in cases]
|
|
1533
|
+
subject_value = self._transform_expression(subject)
|
|
1534
|
+
read_again_as_is = isinstance(subject_value, Literal) or (
|
|
1535
|
+
isinstance(subject_value, Identifier) and '.' not in subject_value.name)
|
|
1536
|
+
if read_again_as_is:
|
|
1537
|
+
reads = [subject_value] + [dataclasses.replace(subject_value) for _ in cases[1:]]
|
|
1538
|
+
else:
|
|
1539
|
+
name = self._fresh_temporary_name('switch_subject')
|
|
1540
|
+
reads = [NamedExpression(name=name, value=subject_value)] + [Identifier(name=name) for _ in cases[1:]]
|
|
1541
|
+
return [BinaryOp(left=read, op='==', right=self._transform_expression(case))
|
|
1542
|
+
for read, (case, _) in zip(reads, cases)]
|
|
1543
|
+
|
|
1544
|
+
def _legacy_bool_cast(self, operand: Expression) -> TernaryOp:
|
|
1545
|
+
"""Pine v5 `bool(x)`: `na(bool) if na(x) else bool(x)`. MEASURED on TradingView
|
|
1546
|
+
(2026-10-05, v5): `bool(1e-10)`, `bool(-1e-10)` and `bool(1.000001e-10)` are
|
|
1547
|
+
true, `bool(0.0)` is false, and `f(bool(2))` selects a bool overload; v5 has no
|
|
1548
|
+
1e-10 tolerance, unlike v6's `cast_bool`. MEASURED again (2026-10-06, probe6):
|
|
1549
|
+
`bool()` of an na float or an na bool is na, typed or untyped, so the na branch is
|
|
1550
|
+
`na(bool)`, not False (it reads as False wherever a condition reads it); that is
|
|
1551
|
+
Pine v5's three-state bool, which the decorator's `na_bool=True` switches on. The
|
|
1552
|
+
card #79 reading `na=false` for `b_na = bool(naF)` is superseded (its script shape
|
|
1553
|
+
differed; the cause is UNPROVEN). Python's `bool` is true for nan, hence the `na`
|
|
1554
|
+
test. An operand that is not a name or a literal is evaluated once, into a temporary."""
|
|
1555
|
+
value = self._transform_expression(operand)
|
|
1556
|
+
if isinstance(value, Literal) or (isinstance(value, Identifier) and '.' not in value.name):
|
|
1557
|
+
tested, read = value, dataclasses.replace(value)
|
|
1558
|
+
else:
|
|
1559
|
+
temporary = self._fresh_temporary_name('bool_operand')
|
|
1560
|
+
tested, read = NamedExpression(name=temporary, value=value), Identifier(name=temporary)
|
|
1561
|
+
self.import_resolver.add_module('na')
|
|
1562
|
+
return TernaryOp(condition=FunctionCall(func='na', args=[tested], kwargs={}),
|
|
1563
|
+
true_expr=self._na_initialiser('bool'),
|
|
1564
|
+
false_expr=FunctionCall(func='bool', args=[read], kwargs={}))
|
|
1565
|
+
|
|
1566
|
+
def _free_spelling(self, preferred: str) -> str:
|
|
1567
|
+
"""`preferred` when no name of the script or the module's own imports holds it, else the
|
|
1568
|
+
lowest free suffix of it; every spelling this returns is reserved for the rest of the run."""
|
|
1569
|
+
if self._identifier_names is None:
|
|
1570
|
+
self._identifier_names = (identifier_names(self._names_source) | EMITTED_HELPER_NAMES
|
|
1571
|
+
| set(self.import_resolver.aliased_modules.values()))
|
|
1572
|
+
return free_spelling(preferred, self._identifier_names)
|
|
1573
|
+
|
|
1574
|
+
def _fresh_temporary_name(self, stem: str) -> str:
|
|
1575
|
+
"""A temporary (a switch subject, a method receiver) that no Pine name of the
|
|
1576
|
+
script uses. The names are collected on the first call, so a script without
|
|
1577
|
+
such a temporary pays nothing for them (card #43)."""
|
|
1578
|
+
if self._used_names is None:
|
|
1579
|
+
self._used_names = collect_names(self._names_source)
|
|
1580
|
+
return fresh_name(stem, self._used_names)
|
|
1581
|
+
|
|
1582
|
+
# ========================================================================
|
|
1583
|
+
# Expression transformations
|
|
1584
|
+
# ========================================================================
|
|
1585
|
+
|
|
1586
|
+
def _transform_expression(self, expr: Expression) -> Expression:
|
|
1587
|
+
"""Transform expression."""
|
|
1588
|
+
if isinstance(expr, BinaryOp):
|
|
1589
|
+
if expr.op in ('and', 'or'):
|
|
1590
|
+
# an operand is cast only beside one PyneComp has a type for: `s and x` with `x` an untyped
|
|
1591
|
+
# parameter casts neither (probes cb16, cb17)
|
|
1592
|
+
typing = getattr(self, '_condition_typing', None)
|
|
1593
|
+
left = (self._condition(expr.left) if typing is None or typing.has_a_type(expr.right)
|
|
1594
|
+
else self._transform_expression(expr.left))
|
|
1595
|
+
right = (self._condition(expr.right) if typing is None or typing.has_a_type(expr.left)
|
|
1596
|
+
else self._transform_expression(expr.right))
|
|
1597
|
+
elif expr.op in ('==', '!=') and self._compares_a_bool(expr):
|
|
1598
|
+
# `flag == 1`: the number compared with a bool is cast (PyneComp)
|
|
1599
|
+
left, right = self._condition(expr.left), self._condition(expr.right)
|
|
1600
|
+
else:
|
|
1601
|
+
left, right = self._transform_expression(expr.left), self._transform_expression(expr.right)
|
|
1602
|
+
return BinaryOp(
|
|
1603
|
+
left=left,
|
|
1604
|
+
op=expr.op,
|
|
1605
|
+
right=right,
|
|
1606
|
+
# Pine v4/v5 `x += y` is printed `x = x + y` (PyneComp: 344 of 346 sites)
|
|
1607
|
+
compound=expr.compound and self.pine_version >= 6,
|
|
1608
|
+
)
|
|
1609
|
+
elif isinstance(expr, UnaryOp):
|
|
1610
|
+
return UnaryOp(
|
|
1611
|
+
op=expr.op,
|
|
1612
|
+
operand=(self._condition(expr.operand, negated=True) if expr.op == 'not'
|
|
1613
|
+
else self._transform_expression(expr.operand))
|
|
1614
|
+
)
|
|
1615
|
+
elif isinstance(expr, TernaryOp):
|
|
1616
|
+
return self._transform_ternary(expr)
|
|
1617
|
+
elif isinstance(expr, FunctionCall):
|
|
1618
|
+
return self._transform_function_call(expr)
|
|
1619
|
+
elif isinstance(expr, MethodCall):
|
|
1620
|
+
return self._transform_method_call(expr)
|
|
1621
|
+
elif isinstance(expr, MemberAccess):
|
|
1622
|
+
direct = self._directly_called_method(expr.object)
|
|
1623
|
+
if direct is not None:
|
|
1624
|
+
return MemberAccess(object=direct, member=expr.member)
|
|
1625
|
+
return self._transform_member_access(expr)
|
|
1626
|
+
elif isinstance(expr, IndexAccess):
|
|
1627
|
+
return self._transform_index_access(expr)
|
|
1628
|
+
elif isinstance(expr, Identifier):
|
|
1629
|
+
return self._transform_identifier(expr)
|
|
1630
|
+
elif isinstance(expr, Literal):
|
|
1631
|
+
return self._transform_literal(expr)
|
|
1632
|
+
elif isinstance(expr, NaLiteral):
|
|
1633
|
+
return self._transform_na_literal(expr)
|
|
1634
|
+
elif isinstance(expr, ArrayLiteral):
|
|
1635
|
+
return ArrayLiteral(elements=[self._transform_expression(e) for e in expr.elements])
|
|
1636
|
+
elif isinstance(expr, IfExpression):
|
|
1637
|
+
return self._transform_if_expression(expr)
|
|
1638
|
+
elif isinstance(expr, SwitchExpression):
|
|
1639
|
+
return self._transform_switch_expression(expr)
|
|
1640
|
+
else:
|
|
1641
|
+
return expr
|
|
1642
|
+
|
|
1643
|
+
def _compares_a_bool(self, expr: BinaryOp) -> bool:
|
|
1644
|
+
"""`flag == 1`: one side of an equality is a bool."""
|
|
1645
|
+
typing = getattr(self, '_condition_typing', None)
|
|
1646
|
+
if typing is not None and (typing.is_bool(expr.left) or typing.is_bool(expr.right)):
|
|
1647
|
+
return True
|
|
1648
|
+
return 'bool' in (self.type_inference.infer_type_hint(expr.left), self.type_inference.infer_type_hint(expr.right))
|
|
1649
|
+
|
|
1650
|
+
def _transform_ternary(self, expr: TernaryOp) -> TernaryOp:
|
|
1651
|
+
"""Transform ternary operator (Rule 10): cond ? a : b => a if cond else b"""
|
|
1652
|
+
# Note: We keep it as TernaryOp, codegen will convert to Python syntax
|
|
1653
|
+
return TernaryOp(
|
|
1654
|
+
condition=self._condition(expr.condition, ternary=expr),
|
|
1655
|
+
true_expr=self._transform_expression(expr.true_expr),
|
|
1656
|
+
false_expr=self._transform_expression(expr.false_expr)
|
|
1657
|
+
)
|
|
1658
|
+
|
|
1659
|
+
def _is_generic_array_of_objects(self, func_name: str, call: FunctionCall) -> bool:
|
|
1660
|
+
"""`array.new<T>(...)` with arguments, for an element type that has no
|
|
1661
|
+
`array.new_*` factory (a UDT, an enum, `chart.point`, ...). A type the script
|
|
1662
|
+
neither declares nor imports is refused at its line (card #81). Without
|
|
1663
|
+
arguments it is `array.new(0, NA(T))`, as PyneComp writes it (probe gen1)."""
|
|
1664
|
+
if not func_name.startswith('array.new<'):
|
|
1665
|
+
return False
|
|
1666
|
+
element_type = func_name[func_name.index('<') + 1:func_name.rindex('>')].strip()
|
|
1667
|
+
if element_type in ARRAY_FACTORY_ELEMENT_TYPES:
|
|
1668
|
+
return False
|
|
1669
|
+
known = self._array_object_element_types()
|
|
1670
|
+
library_type = '.' in element_type and element_type[:element_type.index('.') + 1] in known
|
|
1671
|
+
if element_type not in known and not library_type:
|
|
1672
|
+
raise TransformerError(f"array.new<{element_type}>: '{element_type}' is not a type the script "
|
|
1673
|
+
'declares or imports, nor a built-in type', call.line, call.column)
|
|
1674
|
+
return True
|
|
1675
|
+
|
|
1676
|
+
def _array_object_element_types(self) -> set:
|
|
1677
|
+
"""The script's UDTs and enums, the built-in object types without an
|
|
1678
|
+
`array.new_*` factory, and each imported library alias (its `alias.Type`)."""
|
|
1679
|
+
if self._object_element_types is None:
|
|
1680
|
+
self._object_element_types = (
|
|
1681
|
+
{declaration.name for declaration in self._names_source.declarations
|
|
1682
|
+
if isinstance(declaration, (TypeDecl, EnumDecl))}
|
|
1683
|
+
| BUILTIN_OBJECT_ELEMENT_TYPES
|
|
1684
|
+
| {f'{library.alias or library.library}.' for library in self._names_source.imports})
|
|
1685
|
+
return self._object_element_types
|
|
1686
|
+
|
|
1687
|
+
def _generic_array_new(self, call: FunctionCall, func_name: str = '') -> FunctionCall:
|
|
1688
|
+
"""`array.new(size, value)`: the runtime repeats the one value it is given, so
|
|
1689
|
+
every element is the same object (MEASURED on TradingView, 2026-10-05: after
|
|
1690
|
+
`arr.get(0).v := 99` every element reads 99). The value is evaluated once,
|
|
1691
|
+
at any size; an omitted value is `na` (card #81)."""
|
|
1692
|
+
args = [self._transform_expression(argument) for argument in call.args]
|
|
1693
|
+
kwargs = {name: self._transform_expression(value) for name, value in call.kwargs.items()}
|
|
1694
|
+
if not args and not kwargs:
|
|
1695
|
+
args = [Literal(value=0, literal_type='int')] # `array.new<T>()`: `array.new(0, NA(T))` (PyneComp)
|
|
1696
|
+
if len(args) + len(kwargs) == 1 and 'initial_value' not in kwargs: # a lone size gets T's na
|
|
1697
|
+
value = self._typed_na(func_name)
|
|
1698
|
+
if args:
|
|
1699
|
+
args.append(value)
|
|
1700
|
+
else:
|
|
1701
|
+
kwargs['initial_value'] = value
|
|
1702
|
+
self.import_resolver.add_module('array')
|
|
1703
|
+
return FunctionCall(func='array.new', args=args, kwargs=kwargs)
|
|
1704
|
+
|
|
1705
|
+
def _typed_na(self, func_name: str) -> Expression:
|
|
1706
|
+
"""`NA(T)` for the element type of `array.new<T>`/`matrix.new<T>`; `na` when it has none."""
|
|
1707
|
+
if '<' not in func_name:
|
|
1708
|
+
self.import_resolver.add_module('na')
|
|
1709
|
+
return Identifier(name='na')
|
|
1710
|
+
element = func_name[func_name.index('<') + 1:func_name.rindex('>')].strip()
|
|
1711
|
+
python_type = convert_pine_generic_to_python(element)
|
|
1712
|
+
self.import_resolver.uses_na = True
|
|
1713
|
+
if python_type == 'ChartPoint':
|
|
1714
|
+
self.import_resolver.chart_types.add('ChartPoint')
|
|
1715
|
+
return FunctionCall(func='NA', args=[Identifier(name=python_type)], kwargs={})
|
|
1716
|
+
|
|
1717
|
+
def _generic_matrix_new(self, call: FunctionCall, func_name: str) -> FunctionCall:
|
|
1718
|
+
"""`matrix.new<T>(rows, columns[, value])`: without a value PyneComp passes
|
|
1719
|
+
`initial_value=NA(T)`; an integer value of a float matrix is written as a float (probe gen1)."""
|
|
1720
|
+
element = func_name[func_name.index('<') + 1:func_name.rindex('>')].strip()
|
|
1721
|
+
args = [self._transform_expression(argument) for argument in call.args]
|
|
1722
|
+
kwargs = {name: self._transform_expression(value) for name, value in call.kwargs.items()}
|
|
1723
|
+
if len(args) < 3 and 'initial_value' not in kwargs:
|
|
1724
|
+
kwargs['initial_value'] = self._typed_na(func_name)
|
|
1725
|
+
elif len(args) >= 3 and isinstance(call.args[2], NaLiteral):
|
|
1726
|
+
args[2] = self._typed_na(func_name) # `matrix.new<string>(r, c, na)`: NA(str) (probe loop10)
|
|
1727
|
+
elif element == 'float' and len(args) >= 3 and isinstance(args[2], Literal) \
|
|
1728
|
+
and args[2].literal_type == 'int':
|
|
1729
|
+
args[2] = Literal(value=float(args[2].value), literal_type='float')
|
|
1730
|
+
self.import_resolver.add_module('matrix')
|
|
1731
|
+
return FunctionCall(func='matrix.new', args=args, kwargs=kwargs)
|
|
1732
|
+
|
|
1733
|
+
def _transform_generic_function_name(self, func_name: str) -> str:
|
|
1734
|
+
"""Transform function name with generics."""
|
|
1735
|
+
if '<' not in func_name:
|
|
1736
|
+
return func_name
|
|
1737
|
+
|
|
1738
|
+
# For map.new<K,V>() -> map.new() (just strip generics)
|
|
1739
|
+
if 'map.new<' in func_name or func_name.startswith('map.new<'):
|
|
1740
|
+
return 'map.new'
|
|
1741
|
+
|
|
1742
|
+
# For array.new<T>() -> array.new_T()
|
|
1743
|
+
if 'array.new<' in func_name or func_name.startswith('array.new<'):
|
|
1744
|
+
# Extract the type from array.new<type>
|
|
1745
|
+
start = func_name.index('<') + 1
|
|
1746
|
+
end = func_name.index('>')
|
|
1747
|
+
type_param = func_name[start:end].strip()
|
|
1748
|
+
|
|
1749
|
+
# Map Pine Script types to PyneCore array.new_* functions; any other
|
|
1750
|
+
# declared type with arguments is routed to array.new() before this (card #81)
|
|
1751
|
+
return ARRAY_FACTORIES.get(type_param, 'list')
|
|
1752
|
+
|
|
1753
|
+
# For matrix.new<T>() -> matrix.new() (just strip generics)
|
|
1754
|
+
if 'matrix.new<' in func_name or func_name.startswith('matrix.new<'):
|
|
1755
|
+
return 'matrix.new'
|
|
1756
|
+
|
|
1757
|
+
# For other generic functions, strip generics for now
|
|
1758
|
+
base_name = func_name[:func_name.index('<')]
|
|
1759
|
+
return base_name
|
|
1760
|
+
|
|
1761
|
+
def _parse_dotted_path(self, path: str) -> Expression:
|
|
1762
|
+
"""Parse a dotted path string like 'obj.lbl' into nested MemberAccess expressions."""
|
|
1763
|
+
parts = path.split('.')
|
|
1764
|
+
if len(parts) == 1:
|
|
1765
|
+
return Identifier(name=parts[0])
|
|
1766
|
+
|
|
1767
|
+
# Build nested MemberAccess from left to right
|
|
1768
|
+
expr = Identifier(name=parts[0])
|
|
1769
|
+
for part in parts[1:]:
|
|
1770
|
+
expr = MemberAccess(object=expr, member=part)
|
|
1771
|
+
return expr
|
|
1772
|
+
|
|
1773
|
+
@staticmethod
|
|
1774
|
+
def _module_of_type(type_str: Optional[str]) -> Optional[str]:
|
|
1775
|
+
"""The builtin namespace of a declared receiver type (`list[float]` -> 'array'),
|
|
1776
|
+
or None for an unknown, UDT or scalar type. Callers pass the type already
|
|
1777
|
+
unwrapped (TypeInference.declared_type_name)."""
|
|
1778
|
+
if not type_str:
|
|
1779
|
+
return None
|
|
1780
|
+
if type_str.startswith('list'):
|
|
1781
|
+
return 'array'
|
|
1782
|
+
if type_str.startswith('dict'):
|
|
1783
|
+
return 'map'
|
|
1784
|
+
if type_str in ('line', 'label', 'box', 'table', 'linefill', 'polyline',
|
|
1785
|
+
'Line', 'Label', 'Box', 'Table', 'LineFill', 'Polyline'):
|
|
1786
|
+
return type_str.lower()
|
|
1787
|
+
if type_str.startswith('matrix') or type_str.startswith('Matrix'):
|
|
1788
|
+
return 'matrix'
|
|
1789
|
+
if '<' in type_str:
|
|
1790
|
+
return type_str[:type_str.index('<')]
|
|
1791
|
+
return None
|
|
1792
|
+
|
|
1793
|
+
def _resolve_module_for_method(self, method_name: str, receiver_type: Optional[str]) -> Optional[str]:
|
|
1794
|
+
"""The one routing rule for a builtin method, whatever the receiver's shape (card #8):
|
|
1795
|
+
the namespace of the receiver's declared type when it defines the method; else the
|
|
1796
|
+
one method namespace that defines it, statically; else RUNTIME_METHOD_DISPATCH when
|
|
1797
|
+
several do (pynecore picks by the receiver's class); None when none does."""
|
|
1798
|
+
namespaces = method_namespaces(method_name)
|
|
1799
|
+
typed_module = self._module_of_type(receiver_type)
|
|
1800
|
+
if typed_module in namespaces:
|
|
1801
|
+
return typed_module
|
|
1802
|
+
if len(namespaces) == 1:
|
|
1803
|
+
return namespaces[0]
|
|
1804
|
+
return RUNTIME_METHOD_DISPATCH if namespaces else None
|
|
1805
|
+
|
|
1806
|
+
def _receiver_type(self, receiver) -> Optional[str]:
|
|
1807
|
+
"""Declared type of a method-call receiver in the current scope, or None."""
|
|
1808
|
+
return self.type_inference.infer_receiver_type(
|
|
1809
|
+
receiver, self.current_function_params, self.current_function_shadowed_names)
|
|
1810
|
+
|
|
1811
|
+
def _resolve_module_for_receiver(self, method_name: str, receiver) -> Optional[str]:
|
|
1812
|
+
"""Module for `receiver.method_name()` by the routing rule, from the receiver's type. A receiver
|
|
1813
|
+
PyneComp has no declared type for goes through `method_call`, even where one namespace
|
|
1814
|
+
defines the method (probe mc1: `l = line.new(...)`, `l.set_x2(x)`)."""
|
|
1815
|
+
receiver_type = self._routing_receiver_type(receiver)
|
|
1816
|
+
namespaces = method_namespaces(method_name)
|
|
1817
|
+
if receiver_type is None and namespaces:
|
|
1818
|
+
return RUNTIME_METHOD_DISPATCH
|
|
1819
|
+
return self._resolve_module_for_method(method_name, receiver_type)
|
|
1820
|
+
|
|
1821
|
+
_COLLECTION_CONSTRUCTORS = ('array.new', 'array.from', 'map.new', 'matrix.new')
|
|
1822
|
+
|
|
1823
|
+
def _routing_receiver_type(self, receiver) -> Optional[str]:
|
|
1824
|
+
"""The receiver type PyneComp routes `receiver.method()` by: only a type it reads off a
|
|
1825
|
+
declaration (probe scripts mc1/mc2, PyneComp v6.0.80). A name declared with a type, a typed
|
|
1826
|
+
parameter, a name whose value is a collection constructor (`array.new<line>()`,
|
|
1827
|
+
`array.from(...)`, `map.new<K, V>()`) or a UDT's `T.new()`, a field of such a value and a
|
|
1828
|
+
for-in variable over a typed collection have it; a drawing (`l = line.new(...)`), a copy
|
|
1829
|
+
(`l6 = l1`) and a function's result do not: those go through `method_call`."""
|
|
1830
|
+
if isinstance(receiver, IndexAccess):
|
|
1831
|
+
return None # a history read (`a[1].get(i)`) has no declared type (chrono_utils)
|
|
1832
|
+
path = receiver_path(receiver)
|
|
1833
|
+
if not path or path[0] not in self._declared_receiver_names() \
|
|
1834
|
+
or path[0] in getattr(self, '_untyped_parameters', ()):
|
|
1835
|
+
return None
|
|
1836
|
+
receiver_type = self._receiver_type(receiver)
|
|
1837
|
+
if receiver_type is None:
|
|
1838
|
+
# `var w = T.new().init()` (chrono_utils, biased_price_target), or a for-in variable
|
|
1839
|
+
scope = getattr(self, '_typing_scope', None)
|
|
1840
|
+
owner = (scope.name, scope.line) if scope is not None else None
|
|
1841
|
+
constructed = getattr(self, '_constructed_types', {})
|
|
1842
|
+
local = path[0] in self.current_function_shadowed_names or path[0] in (self.current_function_params or {})
|
|
1843
|
+
element = constructed.get((owner, path[0])) or (None if local else constructed.get((None, path[0])))
|
|
1844
|
+
element = element or self._loop_element_type(path[0])
|
|
1845
|
+
if element is not None and len(path) == 1:
|
|
1846
|
+
return element
|
|
1847
|
+
for field in path[1:] if element is not None else ():
|
|
1848
|
+
# a field of a for-in variable (`zigzagObject.zigzagLine`, Trendoscope/Zigzag)
|
|
1849
|
+
element = self.symbol_table.udt_field_types.get(
|
|
1850
|
+
self.type_inference.declared_type_name(element) or element, {}).get(field)
|
|
1851
|
+
if element is None:
|
|
1852
|
+
break
|
|
1853
|
+
else:
|
|
1854
|
+
return element
|
|
1855
|
+
return receiver_type
|
|
1856
|
+
|
|
1857
|
+
def _loop_element_type(self, name: str) -> Optional[str]:
|
|
1858
|
+
"""The element type of the collection a for-in variable named `name` walks, where it types."""
|
|
1859
|
+
for iterable in self._loop_iterables.get(name, ()):
|
|
1860
|
+
container = self._receiver_type(iterable) or ''
|
|
1861
|
+
if container.startswith('list[') and container.endswith(']'):
|
|
1862
|
+
return container[len('list['):-1] # a for-in variable: its collection's element type
|
|
1863
|
+
if container.startswith('dict[') and container.endswith(']') and ',' in container:
|
|
1864
|
+
return container[container.index(',') + 1:-1].strip() # `for [k, v] in m`: the map's values
|
|
1865
|
+
return None
|
|
1866
|
+
|
|
1867
|
+
def _declared_receiver_names(self) -> frozenset:
|
|
1868
|
+
names = getattr(self, '_declared_receivers', None)
|
|
1869
|
+
if names is not None:
|
|
1870
|
+
return names
|
|
1871
|
+
type_names = {node.name for node in self._names_source.declarations if isinstance(node, (TypeDecl, EnumDecl))}
|
|
1872
|
+
|
|
1873
|
+
aliases = set(self._library_aliases())
|
|
1874
|
+
|
|
1875
|
+
def constructed_type(value) -> Optional[str]:
|
|
1876
|
+
"""`T.new()`, `lib.T.new()` or a method chain on one (`T.new().init()`, probe mc5): T."""
|
|
1877
|
+
while isinstance(value, MethodCall):
|
|
1878
|
+
value = value.object
|
|
1879
|
+
if isinstance(value, FunctionCall) and isinstance(value.func, str):
|
|
1880
|
+
head, _, member = value.func.partition('<')[0].rpartition('.')
|
|
1881
|
+
if member == 'new' and (head in type_names or (head.partition('.')[0] in aliases
|
|
1882
|
+
and head.partition('.')[2][:1].isupper())):
|
|
1883
|
+
return head
|
|
1884
|
+
return None
|
|
1885
|
+
|
|
1886
|
+
drawings = {'line': 'Line', 'label': 'Label', 'box': 'Box', 'table': 'Table', 'linefill': 'LineFill',
|
|
1887
|
+
'polyline': 'Polyline'}
|
|
1888
|
+
|
|
1889
|
+
def all_drawings(value) -> Optional[str]:
|
|
1890
|
+
"""`line.all`: list[Line] (toodegrees/forex_factory_utility)."""
|
|
1891
|
+
dotted = value.name if isinstance(value, Identifier) else (
|
|
1892
|
+
f'{value.object}.{value.member}' if isinstance(value, MemberAccess) and isinstance(value.object, str)
|
|
1893
|
+
else None)
|
|
1894
|
+
if dotted and dotted.endswith('.all') and dotted[:-len('.all')] in drawings:
|
|
1895
|
+
return f'list[{drawings[dotted[:-len(".all")]]}]'
|
|
1896
|
+
return None
|
|
1897
|
+
|
|
1898
|
+
def typed_value(value) -> bool:
|
|
1899
|
+
if isinstance(value, FunctionCall) and isinstance(value.func, str) \
|
|
1900
|
+
and value.func.startswith(self._COLLECTION_CONSTRUCTORS):
|
|
1901
|
+
return True
|
|
1902
|
+
return constructed_type(value) is not None or all_drawings(value) is not None
|
|
1903
|
+
|
|
1904
|
+
declared = set()
|
|
1905
|
+
loops = []
|
|
1906
|
+
# the type a name is constructed with, per function (`var a = array.new<float>()`, `var w = T.new()`)
|
|
1907
|
+
self._constructed_types = {}
|
|
1908
|
+
owners = {}
|
|
1909
|
+
for function in walk(self._names_source):
|
|
1910
|
+
if isinstance(function, FuncDecl):
|
|
1911
|
+
for inner in walk(function.body):
|
|
1912
|
+
owners.setdefault(id(inner), (function.name, function.line))
|
|
1913
|
+
for node in walk(self._names_source):
|
|
1914
|
+
if isinstance(node, (Assignment, VarDecl, VaripDecl)):
|
|
1915
|
+
name = node.target if isinstance(node, Assignment) else node.name
|
|
1916
|
+
if isinstance(name, str) and (getattr(node, 'type_hint', None) or typed_value(node.value)):
|
|
1917
|
+
declared.add(name)
|
|
1918
|
+
key = (owners.get(id(node)), name)
|
|
1919
|
+
if constructed_type(node.value):
|
|
1920
|
+
self._constructed_types.setdefault(key, constructed_type(node.value))
|
|
1921
|
+
elif all_drawings(node.value):
|
|
1922
|
+
self._constructed_types.setdefault(key, all_drawings(node.value))
|
|
1923
|
+
elif isinstance(node.value, FunctionCall) and isinstance(node.value.func, str) \
|
|
1924
|
+
and re.fullmatch(r'(array|map|matrix)\.new<.+>', node.value.func):
|
|
1925
|
+
# `var a = array.new<float>()` in a block (series_collection)
|
|
1926
|
+
generic = node.value.func.replace('.new', '', 1)
|
|
1927
|
+
self._constructed_types.setdefault(key, convert_pine_generic_to_python(generic))
|
|
1928
|
+
elif isinstance(node.value, FunctionCall) and node.value.func == 'array.from' and node.value.args:
|
|
1929
|
+
element = self.type_inference.infer_type_hint(node.value.args[0]) # `array.from(0., 1.)`
|
|
1930
|
+
if element in ('int', 'float', 'bool', 'string', 'color'):
|
|
1931
|
+
self._constructed_types.setdefault(key, convert_pine_generic_to_python(f'array<{element}>'))
|
|
1932
|
+
elif isinstance(node, InputDecl):
|
|
1933
|
+
declared.add(node.name)
|
|
1934
|
+
elif isinstance(node, FuncDecl):
|
|
1935
|
+
declared.update(parameter.name for parameter in node.params if parameter.type_hint)
|
|
1936
|
+
elif isinstance(node, ForInLoop):
|
|
1937
|
+
loops.append(node)
|
|
1938
|
+
self._loop_iterables = {}
|
|
1939
|
+
for loop in loops: # a for-in variable over a typed collection
|
|
1940
|
+
path = receiver_path(loop.iterable)
|
|
1941
|
+
if path and path[0] in declared:
|
|
1942
|
+
declared.update(loop.vars)
|
|
1943
|
+
# every loop of the name, any function: the one whose iterable types where it is read wins
|
|
1944
|
+
if len(loop.vars) == 1:
|
|
1945
|
+
self._loop_iterables.setdefault(loop.vars[0], []).append(loop.iterable)
|
|
1946
|
+
elif len(loop.vars) == 2: # `for [k, v] in m`: the value is an element
|
|
1947
|
+
self._loop_iterables.setdefault(loop.vars[1], []).append(loop.iterable)
|
|
1948
|
+
self._declared_receivers = frozenset(declared)
|
|
1949
|
+
return self._declared_receivers
|
|
1950
|
+
|
|
1951
|
+
def _method_signatures(self, method_name: str) -> List[tuple]:
|
|
1952
|
+
"""(`this` type, required, total arguments after `this`) of every user method named `method_name`."""
|
|
1953
|
+
return [(parameters[0].type_hint, sum(1 for parameter in parameters[1:] if parameter.default is None),
|
|
1954
|
+
len(parameters) - 1)
|
|
1955
|
+
for node in walk(self._names_source)
|
|
1956
|
+
if isinstance(node, FuncDecl) and node.is_method and node.name == method_name
|
|
1957
|
+
for parameters in [node.params] if parameters]
|
|
1958
|
+
|
|
1959
|
+
def _user_method_route(self, method_name: str, receiver, call=None) -> Optional[str]:
|
|
1960
|
+
"""How `receiver.method_name()` reaches a user method of that name (card #8):
|
|
1961
|
+
USER_METHOD, RUNTIME_CHOICE, or None for the builtin route (also when no user
|
|
1962
|
+
method has the name). A typed receiver takes the user method unless its builtin
|
|
1963
|
+
namespace defines the name and no user method is declared on that namespace's
|
|
1964
|
+
type. An untyped receiver whose method name a UDT method shares with a builtin
|
|
1965
|
+
namespace is decided at run time by its class."""
|
|
1966
|
+
this_types = self.method_functions.get(method_name)
|
|
1967
|
+
if this_types is None:
|
|
1968
|
+
return None
|
|
1969
|
+
namespaces = method_namespaces(method_name)
|
|
1970
|
+
receiver_type = self._routing_receiver_type(receiver)
|
|
1971
|
+
receiver_module = self._module_of_type(receiver_type)
|
|
1972
|
+
if not namespaces or (receiver_type and receiver_module not in namespaces):
|
|
1973
|
+
return USER_METHOD
|
|
1974
|
+
if receiver_type:
|
|
1975
|
+
# a builtin namespace has the name too: the user method only for its own `this` type and a call it
|
|
1976
|
+
# takes (Trendoscope/Zigzag: `values.push(x)` on array<float> is array.push, `this.clear()` on
|
|
1977
|
+
# array<ZigzagDrawing> the user method)
|
|
1978
|
+
wanted = _spelled_type(receiver_type)
|
|
1979
|
+
arguments = None if call is None else len(call.args) + len(call.kwargs)
|
|
1980
|
+
return USER_METHOD if any(
|
|
1981
|
+
_spelled_type(this_type) == wanted and (arguments is None or required <= arguments <= total)
|
|
1982
|
+
for this_type, required, total in self._method_signatures(method_name)) else None
|
|
1983
|
+
udt_names = self.symbol_table.udt_field_types
|
|
1984
|
+
return RUNTIME_CHOICE if all(this_type in udt_names for this_type in this_types) else USER_METHOD
|
|
1985
|
+
|
|
1986
|
+
def _route_method_call(self, call, method_name: str, receiver, make_receiver) -> Optional[Expression]:
|
|
1987
|
+
"""`receiver.method_name(...)` as a function call, or None when no route applies.
|
|
1988
|
+
`receiver` is the Pine receiver (a node or dotted path); `make_receiver()` builds
|
|
1989
|
+
a fresh transformed receiver expression each time it is called."""
|
|
1990
|
+
user_methods = self._user_methods()
|
|
1991
|
+
# on a value that is no name (`T.new().m()`, `matrix.row(m, r).nonZero()`, `(a * b).m()`) PyneComp
|
|
1992
|
+
# calls the method directly only when every declaration of its name is exported (probe mc5, the
|
|
1993
|
+
# reference's jason5480 and Trendoscope libraries)
|
|
1994
|
+
typed_arm = isinstance(receiver, TernaryOp) and any(
|
|
1995
|
+
receiver_path(arm) is not None and self._has_a_known_type(arm)
|
|
1996
|
+
for arm in (receiver.true_expr, receiver.false_expr)) # `(c ? T.new() : this.t).m()` (chrono_utils)
|
|
1997
|
+
if typed_arm and method_name in user_methods:
|
|
1998
|
+
return self._user_method_call(method_name, receiver, make_receiver(), call)
|
|
1999
|
+
# an inline input hoisted into a parameter (`input.enum(...).to_string()`) is still a call's result
|
|
2000
|
+
hoisted_input = isinstance(receiver, Identifier) and re.fullmatch(r'\w+__input|__input_\d+__', receiver.name)
|
|
2001
|
+
if (receiver_path(receiver) is None and not isinstance(receiver, Literal)) or hoisted_input:
|
|
2002
|
+
dispatched = method_name in user_methods and not user_methods[method_name]
|
|
2003
|
+
if method_name in user_methods and not dispatched: # `clear(delete(this))` (Trendoscope/Drawing)
|
|
2004
|
+
return self._user_method_call(method_name, receiver, make_receiver(), call)
|
|
2005
|
+
else:
|
|
2006
|
+
dispatched = method_name in user_methods and not self._has_a_known_type(receiver)
|
|
2007
|
+
if dispatched:
|
|
2008
|
+
# PyneComp: a user method on a receiver it has no declared type for is chosen at run time,
|
|
2009
|
+
# `method_call(m, x)`; an exported method by its name, `method_call('m', x)` (probes mc3, mc4)
|
|
2010
|
+
args = [make_receiver()] + [self._transform_expression(arg) for arg in call.args]
|
|
2011
|
+
kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
|
|
2012
|
+
callee = (Literal(value=method_name, literal_type='string') if user_methods[method_name]
|
|
2013
|
+
else Identifier(name=method_name))
|
|
2014
|
+
self.import_resolver.uses_method_call = True
|
|
2015
|
+
return FunctionCall(func=RUNTIME_METHOD_DISPATCH, args=[callee] + args, kwargs=kwargs)
|
|
2016
|
+
user_route = self._user_method_route(method_name, receiver, call)
|
|
2017
|
+
if user_route == USER_METHOD:
|
|
2018
|
+
return self._user_method_call(method_name, receiver, make_receiver(), call)
|
|
2019
|
+
if user_route == RUNTIME_CHOICE:
|
|
2020
|
+
return self._runtime_chosen_method_call(method_name, receiver, make_receiver, call)
|
|
2021
|
+
module_name = self._resolve_module_for_receiver(method_name, receiver)
|
|
2022
|
+
if module_name is None and method_name not in user_methods and not method_namespaces(method_name) \
|
|
2023
|
+
and self._library_aliases():
|
|
2024
|
+
# a method an imported library exports (`labels.flush()`): chosen at run time (Trendoscope/Zigzag)
|
|
2025
|
+
module_name = RUNTIME_METHOD_DISPATCH
|
|
2026
|
+
if module_name is None:
|
|
2027
|
+
return None
|
|
2028
|
+
return self._builtin_method_call(module_name, method_name, make_receiver(), call)
|
|
2029
|
+
|
|
2030
|
+
def _has_a_known_type(self, receiver) -> bool:
|
|
2031
|
+
"""A receiver whose type PyneComp knows for a user method: a declared one, an input, a
|
|
2032
|
+
built-in series (`close.f()`) or a literal (probes mc3, mc4 and the reference)."""
|
|
2033
|
+
if isinstance(receiver, (Literal,)) or self._routing_receiver_type(receiver) is not None:
|
|
2034
|
+
return True
|
|
2035
|
+
path = receiver_path(receiver)
|
|
2036
|
+
if not path or len(path) != 1:
|
|
2037
|
+
return False
|
|
2038
|
+
name = path[0]
|
|
2039
|
+
if name in SERIES_VARIABLES:
|
|
2040
|
+
return True
|
|
2041
|
+
return any(isinstance(node, InputDecl) and node.name == name for node in self._names_source.declarations)
|
|
2042
|
+
|
|
2043
|
+
def _user_methods(self) -> Dict[str, bool]:
|
|
2044
|
+
"""Every user method's name -> whether all its declarations are exported (PyneComp then names
|
|
2045
|
+
it `method_call('m', x)`, else `method_call(m, x)`: probes mc4, mc5)."""
|
|
2046
|
+
methods = getattr(self, '_user_method_names', None)
|
|
2047
|
+
if methods is None:
|
|
2048
|
+
methods = {}
|
|
2049
|
+
for node in walk(self._names_source):
|
|
2050
|
+
if isinstance(node, FuncDecl) and node.is_method:
|
|
2051
|
+
methods[node.name] = methods.get(node.name, True) and node.is_export
|
|
2052
|
+
self._user_method_names = methods
|
|
2053
|
+
return methods
|
|
2054
|
+
|
|
2055
|
+
def _user_method_call(self, method_name: str, receiver, receiver_expr, call) -> FunctionCall:
|
|
2056
|
+
"""`method_name(receiver, ...)`, a call of the user method."""
|
|
2057
|
+
pine_receiver = self._parse_dotted_path(receiver) if isinstance(receiver, str) else receiver
|
|
2058
|
+
args = [receiver_expr] + [self._transform_expression(arg) for arg in call.args]
|
|
2059
|
+
kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
|
|
2060
|
+
args, kwargs = self._overload_arguments.coerce(
|
|
2061
|
+
call, method_name, [pine_receiver, *call.args], args, call.kwargs, kwargs)
|
|
2062
|
+
return FunctionCall(func=method_name, args=args, kwargs=kwargs)
|
|
2063
|
+
|
|
2064
|
+
def _builtin_method_call(self, module_name: str, method_name: str, receiver_expr, call) -> FunctionCall:
|
|
2065
|
+
"""`module.method(receiver, ...)`, or `method_call('method', receiver, ...)` to let
|
|
2066
|
+
pynecore pick the namespace from the receiver's class. A keyword the scope
|
|
2067
|
+
renamer renamed for a user method of the same name keeps its Pine name."""
|
|
2068
|
+
args = [receiver_expr] + [self._transform_expression(arg) for arg in call.args]
|
|
2069
|
+
pine_keywords = self._pine_keywords.get(method_name, {})
|
|
2070
|
+
kwargs = {pine_keywords.get(k, k): self._transform_expression(v) for k, v in call.kwargs.items()}
|
|
2071
|
+
if module_name == RUNTIME_METHOD_DISPATCH:
|
|
2072
|
+
self.import_resolver.uses_method_call = True
|
|
2073
|
+
method_literal = Literal(value=method_name, literal_type='string')
|
|
2074
|
+
return FunctionCall(func=RUNTIME_METHOD_DISPATCH, args=[method_literal] + args, kwargs=kwargs)
|
|
2075
|
+
self.import_resolver.add_module(module_name)
|
|
2076
|
+
return FunctionCall(func=f'{module_name}.{method_name}', args=args, kwargs=kwargs)
|
|
2077
|
+
|
|
2078
|
+
def _runtime_chosen_method_call(self, method_name: str, receiver, make_receiver, call) -> TernaryOp:
|
|
2079
|
+
"""`m(x) if isinstance(x, Z) else <builtin route>(x)`: an untyped receiver runs the
|
|
2080
|
+
user method when it is an instance of the method's UDT, else the builtin. A direct
|
|
2081
|
+
call keeps the user method's per-call-site state, which pynecore's callable
|
|
2082
|
+
`method_call(m, x)` would share across call sites. A receiver that is not a name
|
|
2083
|
+
or field path (a call result) is evaluated once, into a temporary."""
|
|
2084
|
+
classes = [Identifier(name=this_type) for this_type in sorted(self.method_functions[method_name])]
|
|
2085
|
+
udt_classes = classes[0] if len(classes) == 1 else ArrayLiteral(elements=classes)
|
|
2086
|
+
builtin_module = self._resolve_module_for_method(method_name, None)
|
|
2087
|
+
if receiver_path(receiver):
|
|
2088
|
+
tested_receiver, read_receiver = make_receiver(), make_receiver
|
|
2089
|
+
else:
|
|
2090
|
+
temporary = self._fresh_temporary_name('method_receiver')
|
|
2091
|
+
tested_receiver = NamedExpression(name=temporary, value=make_receiver())
|
|
2092
|
+
read_receiver = lambda: Identifier(name=temporary) # noqa: E731 - the arms read the temporary
|
|
2093
|
+
return TernaryOp(
|
|
2094
|
+
condition=FunctionCall(func='isinstance', args=[tested_receiver, udt_classes], kwargs={}),
|
|
2095
|
+
true_expr=self._user_method_call(method_name, receiver, read_receiver(), call),
|
|
2096
|
+
false_expr=self._builtin_method_call(builtin_module, method_name, read_receiver(), call))
|
|
2097
|
+
|
|
2098
|
+
def _transform_dotted_string_method_call(self, call: FunctionCall, func: str) -> Optional[Expression]:
|
|
2099
|
+
"""Handle var.method() → module.method(var, ...) for dotted string function names.
|
|
2100
|
+
|
|
2101
|
+
Routes a user method, or a receiver typed as a builtin namespace; returns None
|
|
2102
|
+
for the rest, which the chained handler takes (it skips module names: `math.max`).
|
|
2103
|
+
"""
|
|
2104
|
+
var_name, _, method_name = func.partition('.')
|
|
2105
|
+
if not method_name or '.' in method_name or self._names_a_type(var_name): # Not a variable method call
|
|
2106
|
+
return None
|
|
2107
|
+
if self._names_a_namespace(var_name): # `array.push(a, x)` is a namespace function
|
|
2108
|
+
return None
|
|
2109
|
+
if method_name == 'copy' and not call.args and not call.kwargs and 'copy' not in self._user_methods():
|
|
2110
|
+
self.import_resolver.uses_udt_copy = True # `d.copy()` is `udt_copy(d)`, an array too (reference)
|
|
2111
|
+
return FunctionCall(func='udt_copy', args=[self._transform_expression(Identifier(name=var_name))],
|
|
2112
|
+
kwargs={})
|
|
2113
|
+
if method_name not in self.method_functions and not self._module_of_type(self._receiver_type(var_name)):
|
|
2114
|
+
return None
|
|
2115
|
+
return self._route_method_call(call, method_name, Identifier(name=var_name), lambda: Identifier(name=var_name))
|
|
2116
|
+
|
|
2117
|
+
def _transform_chained_dotted_method_call(self, call: FunctionCall, func: str) -> Optional[Expression]:
|
|
2118
|
+
"""Handle obj.field.method() → module.method(obj.field, ...) for chained access.
|
|
2119
|
+
|
|
2120
|
+
Returns transformed FunctionCall or None if not a chained method pattern.
|
|
2121
|
+
"""
|
|
2122
|
+
all_variable, _, all_method_name = func.rpartition('.')
|
|
2123
|
+
if all_variable in DRAWING_ALL_VARIABLES: # box.all.size() -> array.size(box.all) (#37)
|
|
2124
|
+
return self._builtin_method_call('array', all_method_name, Identifier(name=all_variable), call)
|
|
2125
|
+
|
|
2126
|
+
first_component = func.split('.')[0]
|
|
2127
|
+
if (first_component in KNOWN_MODULES or self._names_a_type(first_component)
|
|
2128
|
+
or first_component in self.import_resolver.aliased_modules.values()
|
|
2129
|
+
or first_component in self._library_aliases()):
|
|
2130
|
+
return None
|
|
2131
|
+
|
|
2132
|
+
obj_path, _, method_name = func.rpartition('.') # "obj.lbl", "set_x"
|
|
2133
|
+
|
|
2134
|
+
# Special case: .copy() on object fields -> udt_copy()
|
|
2135
|
+
# Must be checked BEFORE set matching (Location 1 behavior)
|
|
2136
|
+
if method_name == 'copy' and self._user_method_route(method_name, obj_path) is None:
|
|
2137
|
+
obj_expr = self._parse_dotted_path(obj_path)
|
|
2138
|
+
transformed_args = [obj_expr] + [self._transform_expression(arg) for arg in call.args]
|
|
2139
|
+
transformed_kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
|
|
2140
|
+
self.import_resolver.uses_udt_copy = True
|
|
2141
|
+
return FunctionCall(func='udt_copy', args=transformed_args, kwargs=transformed_kwargs)
|
|
2142
|
+
|
|
2143
|
+
return self._route_method_call(call, method_name, obj_path, lambda: self._parse_dotted_path(obj_path))
|
|
2144
|
+
|
|
2145
|
+
def _directly_called_method(self, value) -> Optional[FunctionCall]:
|
|
2146
|
+
"""`x.m(...)` whose result is read for a field (`buf.rotate(n).data`): PyneComp calls the user method
|
|
2147
|
+
directly, whatever it knows of `x` (jason5480/series_collection)."""
|
|
2148
|
+
if not (isinstance(value, FunctionCall) and isinstance(value.func, str) and '.' in value.func):
|
|
2149
|
+
return None
|
|
2150
|
+
receiver, _, method_name = value.func.rpartition('.')
|
|
2151
|
+
if method_name not in self._user_methods() or self._names_a_namespace(receiver.split('.')[0]) \
|
|
2152
|
+
or self._names_a_type(receiver.split('.')[0]):
|
|
2153
|
+
return None
|
|
2154
|
+
return self._user_method_call(method_name, receiver, self._parse_dotted_path(receiver), value)
|
|
2155
|
+
|
|
2156
|
+
def _names_a_user_type(self, name: str) -> bool:
|
|
2157
|
+
return any(isinstance(node, TypeDecl) and node.name == name for node in self._names_source.declarations)
|
|
2158
|
+
|
|
2159
|
+
def _names_a_type(self, name: str) -> bool:
|
|
2160
|
+
"""`T.new()`, `E.member`: a capitalised name that is no variable of the script (`EMSP.repeat()`
|
|
2161
|
+
on `const string EMSP` is a method call)."""
|
|
2162
|
+
if any(isinstance(node, (TypeDecl, EnumDecl)) and node.name == name for node in self._names_source.declarations):
|
|
2163
|
+
return True
|
|
2164
|
+
return name[:1].isupper() and name not in self._declared_receiver_names() \
|
|
2165
|
+
and self.symbol_table.lookup(name) is None
|
|
2166
|
+
|
|
2167
|
+
def _names_a_namespace(self, name: str) -> bool:
|
|
2168
|
+
"""A pynecore namespace or an imported library alias, not a series variable:
|
|
2169
|
+
`close.m()` is a method call on the series, `math.abs(x)` is not."""
|
|
2170
|
+
return ((name in KNOWN_MODULES and not is_series_variable(name))
|
|
2171
|
+
or name in self.import_resolver.aliased_modules.values()
|
|
2172
|
+
or name in self._library_aliases())
|
|
2173
|
+
|
|
2174
|
+
def _library_aliases(self) -> set:
|
|
2175
|
+
"""The names the script's `import user/lib/version as alias` lines bind (an unaliased import binds the library name):
|
|
2176
|
+
`lib.max(a, b)` calls the library's `max`, it is not a method call on `lib`."""
|
|
2177
|
+
return {imported.alias or imported.library for imported in self._names_source.imports}
|
|
2178
|
+
|
|
2179
|
+
def _transform_member_access_method_call(self, call: FunctionCall, func: MemberAccess) -> Optional[Expression]:
|
|
2180
|
+
"""Handle MemberAccess-based method calls like obj.lbl.set_x(...).
|
|
2181
|
+
|
|
2182
|
+
Returns transformed FunctionCall or None if not a recognized method.
|
|
2183
|
+
"""
|
|
2184
|
+
return self._route_method_call(call, func.member, func.object,
|
|
2185
|
+
lambda: self._transform_expression(func.object))
|
|
2186
|
+
|
|
2187
|
+
def _transform_function_call(self, call: FunctionCall) -> Expression:
|
|
2188
|
+
"""Transform function call (Rules 23-25, 49-51)."""
|
|
2189
|
+
pine_call = call # the node as parsed; `call` may be rebuilt below (card #39)
|
|
2190
|
+
func = call.func
|
|
2191
|
+
pine_name = func if isinstance(func, str) else None
|
|
2192
|
+
if isinstance(func, str):
|
|
2193
|
+
# Apply renamed function mapping (e.g., pass -> pass_)
|
|
2194
|
+
if func in self._renamed_functions:
|
|
2195
|
+
func = self._renamed_functions[func]
|
|
2196
|
+
call = FunctionCall(func=func, args=call.args, kwargs=call.kwargs)
|
|
2197
|
+
|
|
2198
|
+
# Convert excess positional args to keyword args for input.* calls.
|
|
2199
|
+
# Pine Script allows input.int(defval, title, minval, maxval, ...)
|
|
2200
|
+
# but PyneCore requires minval=, maxval=, ... as keyword-only.
|
|
2201
|
+
if is_input_function(func) and len(call.args) > 2:
|
|
2202
|
+
new_args, positional_keywords = bind_input_positional_args(func, call.args)
|
|
2203
|
+
call = FunctionCall(func=func, args=new_args, kwargs={**call.kwargs, **positional_keywords})
|
|
2204
|
+
|
|
2205
|
+
# Handle method calls disguised as dotted function calls
|
|
2206
|
+
if '.' in func and '<' not in func and '.' not in func.split('.')[0]:
|
|
2207
|
+
# Try var.method() → module.method(var, ...)
|
|
2208
|
+
result = self._transform_dotted_string_method_call(call, func)
|
|
2209
|
+
if result:
|
|
2210
|
+
return result
|
|
2211
|
+
|
|
2212
|
+
# Try chained member access: obj.lbl.set_x → label.set_x(obj.lbl, ...)
|
|
2213
|
+
if '.' in func:
|
|
2214
|
+
result = self._transform_chained_dotted_method_call(call, func)
|
|
2215
|
+
if result:
|
|
2216
|
+
return result
|
|
2217
|
+
|
|
2218
|
+
# `array.max(a, nth)` / `array.min(a, nth)` stay as written (PyneComp, jordanfray library)
|
|
2219
|
+
# Handle generic syntax (map.new<K,V>, array.new<T>, etc.)
|
|
2220
|
+
if '<' in func and self._is_generic_array_of_objects(func, call):
|
|
2221
|
+
return self._generic_array_new(call, func)
|
|
2222
|
+
if func.startswith('matrix.new<'):
|
|
2223
|
+
return self._generic_matrix_new(call, func)
|
|
2224
|
+
if '<' in func:
|
|
2225
|
+
func = self._transform_generic_function_name(func)
|
|
2226
|
+
elif func.endswith('.copy') and self._names_a_user_type(func[:-len('.copy')]) and len(call.args) == 1:
|
|
2227
|
+
# `T.copy(x)` is `udt_copy(x)` (PyneComp, Trendoscope/Zigzag)
|
|
2228
|
+
self.import_resolver.uses_udt_copy = True
|
|
2229
|
+
return FunctionCall(func='udt_copy', args=[self._transform_expression(call.args[0])], kwargs={})
|
|
2230
|
+
elif '.new' in func:
|
|
2231
|
+
base_name = func.replace('.new', '')
|
|
2232
|
+
# a UDT constructor, `T.new(...)` -> `T(...)`; a library's type keeps `lib.T.new(...)` (PyneComp)
|
|
2233
|
+
if base_name and '.' not in base_name and (
|
|
2234
|
+
base_name[0].isupper() or base_name in self.symbol_table.udt_field_types):
|
|
2235
|
+
func = base_name
|
|
2236
|
+
elif func in _NA_CAST_TYPES and len(call.args) == 1 and not call.kwargs \
|
|
2237
|
+
and isinstance(call.args[0], NaLiteral):
|
|
2238
|
+
# `float(na)`, `color(na)`, `box(na)`: a typed na, NA(T) (PyneComp)
|
|
2239
|
+
return FunctionCall(func='NA', args=[Identifier(name=_NA_CAST_TYPES[func])], kwargs={})
|
|
2240
|
+
elif func == 'int':
|
|
2241
|
+
func = 'cast_int'
|
|
2242
|
+
self.import_resolver.uses_cast_int = True
|
|
2243
|
+
elif func == 'float':
|
|
2244
|
+
func = 'cast_float'
|
|
2245
|
+
self.import_resolver.uses_cast_float = True
|
|
2246
|
+
elif func == 'bool' and self.bool_is_never_na and len(call.args) + len(call.kwargs) == 1 \
|
|
2247
|
+
and not isinstance([*call.args, *call.kwargs.values()][0], NaLiteral):
|
|
2248
|
+
# Pine v6: na, nan and |x| <= 1e-10 are false; Python's bool(nan) is true (card #56)
|
|
2249
|
+
func = 'cast_bool'
|
|
2250
|
+
self.import_resolver.uses_cast_bool = True
|
|
2251
|
+
elif func == 'bool' and not self.bool_is_never_na and len(call.args) + len(call.kwargs) == 1 \
|
|
2252
|
+
and not isinstance([*call.args, *call.kwargs.values()][0], NaLiteral):
|
|
2253
|
+
# Pine v5 and earlier: any nonzero number is true, 0 and na false (card #79)
|
|
2254
|
+
return self._legacy_bool_cast([*call.args, *call.kwargs.values()][0])
|
|
2255
|
+
elif func in ('color', 'bool', 'string'):
|
|
2256
|
+
# Type cast: color(na) → na, color(x) → x
|
|
2257
|
+
# When arg is na, return plain na (Pine na is untyped at runtime)
|
|
2258
|
+
if len(call.args) == 1 and isinstance(call.args[0], NaLiteral):
|
|
2259
|
+
self.import_resolver.add_module('na')
|
|
2260
|
+
return NaLiteral()
|
|
2261
|
+
# For non-na args, pass through as identity (type casts are no-ops in Python)
|
|
2262
|
+
if len(call.args) == 1:
|
|
2263
|
+
return self._transform_expression(call.args[0])
|
|
2264
|
+
func = func # keep as-is for module usage
|
|
2265
|
+
elif func in DRAWING_CAST_FUNCTIONS and (len(call.args), set(call.kwargs)) in ((1, set()), (0, {'x'})):
|
|
2266
|
+
return self._drawing_type_cast(func, call.args[0] if call.args else call.kwargs['x'])
|
|
2267
|
+
else:
|
|
2268
|
+
func = get_function_name(func)
|
|
2269
|
+
if '.' in func:
|
|
2270
|
+
parts = func.split('.')
|
|
2271
|
+
parts[0] = get_module_name(parts[0])
|
|
2272
|
+
func = '.'.join(parts)
|
|
2273
|
+
|
|
2274
|
+
elif isinstance(func, MemberAccess):
|
|
2275
|
+
result = self._transform_member_access_method_call(call, func)
|
|
2276
|
+
if result:
|
|
2277
|
+
return result
|
|
2278
|
+
|
|
2279
|
+
# Handle generics in member access
|
|
2280
|
+
if '<' in func.member:
|
|
2281
|
+
func.member = self._transform_generic_function_name(f"{func.object if isinstance(func.object, str) else 'obj'}.{func.member}")
|
|
2282
|
+
if func.member in ('dict', 'list'):
|
|
2283
|
+
func = func.member
|
|
2284
|
+
else:
|
|
2285
|
+
func = self._transform_member_access(func)
|
|
2286
|
+
|
|
2287
|
+
# Transform arguments; a condition argument holding a number is cast_bool(...) (PyneComp)
|
|
2288
|
+
condition_parameter = _CONDITION_PARAMETERS.get(func) if isinstance(func, str) else None
|
|
2289
|
+
transformed_args = [self._condition(arg) if condition_parameter and index == 0 else self._transform_expression(arg)
|
|
2290
|
+
for index, arg in enumerate(call.args)]
|
|
2291
|
+
transformed_kwargs = {k: self._condition(v) if k == condition_parameter else self._transform_expression(v)
|
|
2292
|
+
for k, v in call.kwargs.items()}
|
|
2293
|
+
transformed_args, transformed_kwargs = self._overload_arguments.coerce(
|
|
2294
|
+
pine_call, pine_name, call.args, transformed_args, call.kwargs, transformed_kwargs)
|
|
2295
|
+
return FunctionCall(func=func, args=transformed_args, kwargs=transformed_kwargs)
|
|
2296
|
+
|
|
2297
|
+
def _transform_method_call(self, call: MethodCall) -> Union[MethodCall, FunctionCall, TernaryOp]:
|
|
2298
|
+
"""Transform method call (Rule 48)."""
|
|
2299
|
+
if '.' in call.method: # zs.get(0).bx.delete() parses as method 'bx.delete' (#36)
|
|
2300
|
+
*field_names, method_name = call.method.split('.')
|
|
2301
|
+
receiver = call.object
|
|
2302
|
+
for field_name in field_names:
|
|
2303
|
+
receiver = MemberAccess(object=receiver, member=field_name)
|
|
2304
|
+
return self._transform_method_call(dataclasses.replace(call, object=receiver, method=method_name))
|
|
2305
|
+
|
|
2306
|
+
def transformed_receiver():
|
|
2307
|
+
return self._transform_expression(call.object)
|
|
2308
|
+
|
|
2309
|
+
# A user-defined @method, also on an expression: (expr).even_decimal() -> even_decimal(expr).
|
|
2310
|
+
# First, so a user method named like the special cases below still runs.
|
|
2311
|
+
if call.method in self.method_functions:
|
|
2312
|
+
routed = self._route_method_call(call, call.method, call.object, transformed_receiver)
|
|
2313
|
+
if routed is not None:
|
|
2314
|
+
return routed
|
|
2315
|
+
|
|
2316
|
+
# Special case: UDT.new() should become UDT() (remove .new())
|
|
2317
|
+
if call.method == 'new' and isinstance(call.object, Identifier):
|
|
2318
|
+
# Check if this is a UDT type (starts with uppercase)
|
|
2319
|
+
if call.object.name[0].isupper():
|
|
2320
|
+
# Convert to function call: Statistics.new() -> Statistics()
|
|
2321
|
+
args = [self._transform_expression(arg) for arg in call.args]
|
|
2322
|
+
kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
|
|
2323
|
+
return FunctionCall(func=call.object.name, args=args, kwargs=kwargs)
|
|
2324
|
+
|
|
2325
|
+
# Special case: .copy() on UDT object fields should become udt_copy()
|
|
2326
|
+
# Example: this.lbl.copy() -> udt_copy(this.lbl); and on a call's result (`a.get(j).copy()`, PyneComp)
|
|
2327
|
+
# `x.copy()` is `udt_copy(x)` whatever `x` is, an array included (the reference never writes array.copy
|
|
2328
|
+
# for the method form: mlmi `distances.copy()`, forex_factory_utility `N.copy()`)
|
|
2329
|
+
if call.method == 'copy' and not call.args and 'copy' not in self._user_methods():
|
|
2330
|
+
obj_expr = self._transform_expression(call.object)
|
|
2331
|
+
args = [obj_expr] + [self._transform_expression(arg) for arg in call.args]
|
|
2332
|
+
kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
|
|
2333
|
+
self.import_resolver.uses_udt_copy = True
|
|
2334
|
+
return FunctionCall(func='udt_copy', args=args, kwargs=kwargs)
|
|
2335
|
+
|
|
2336
|
+
# A builtin method: id.push(value) -> array.push(id, value)
|
|
2337
|
+
routed = self._route_method_call(call, call.method, call.object, transformed_receiver)
|
|
2338
|
+
if routed is not None:
|
|
2339
|
+
return routed
|
|
2340
|
+
|
|
2341
|
+
obj = self._transform_expression(call.object)
|
|
2342
|
+
args = [self._transform_expression(arg) for arg in call.args]
|
|
2343
|
+
kwargs = {k: self._transform_expression(v) for k, v in call.kwargs.items()}
|
|
2344
|
+
|
|
2345
|
+
return MethodCall(object=obj, method=call.method, args=args, kwargs=kwargs)
|
|
2346
|
+
|
|
2347
|
+
def _transform_member_access(self, expr: MemberAccess) -> MemberAccess:
|
|
2348
|
+
"""Transform member access (Rules 23-25)."""
|
|
2349
|
+
# Handle module renames
|
|
2350
|
+
if isinstance(expr.object, str):
|
|
2351
|
+
obj = get_module_name(expr.object)
|
|
2352
|
+
else:
|
|
2353
|
+
obj = self._transform_expression(expr.object)
|
|
2354
|
+
|
|
2355
|
+
# Handle plot style remapping (Rule 25)
|
|
2356
|
+
if isinstance(expr.object, str):
|
|
2357
|
+
full_name = f"{expr.object}.{expr.member}"
|
|
2358
|
+
if needs_plot_style_remap(full_name):
|
|
2359
|
+
remapped = get_plot_style_remap(full_name)
|
|
2360
|
+
parts = remapped.split('.')
|
|
2361
|
+
return MemberAccess(object=parts[0], member=parts[1])
|
|
2362
|
+
|
|
2363
|
+
return MemberAccess(object=obj, member=expr.member)
|
|
2364
|
+
|
|
2365
|
+
def _transform_index_access(self, expr: IndexAccess) -> Expression:
|
|
2366
|
+
"""Transform index access (Rule 52: warn about negative indices).
|
|
2367
|
+
|
|
2368
|
+
A group or a call as the object of a history index was already bound
|
|
2369
|
+
to a named local by grouped_history_lowering (card #32).
|
|
2370
|
+
"""
|
|
2371
|
+
obj = self._transform_expression(expr.object)
|
|
2372
|
+
index = self._transform_expression(expr.index)
|
|
2373
|
+
if not isinstance(expr.object, (Identifier, MemberAccess)) and not (
|
|
2374
|
+
isinstance(expr.object, str)):
|
|
2375
|
+
# the history of an expression or a call: pynecore's inline_series(expr, n) (PyneComp)
|
|
2376
|
+
return FunctionCall(func='inline_series', args=[obj, index], kwargs={})
|
|
2377
|
+
return IndexAccess(object=obj, index=index)
|
|
2378
|
+
|
|
2379
|
+
def _transform_identifier(self, expr: Identifier) -> Identifier:
|
|
2380
|
+
"""Transform identifier (Rule 23 for module prefixes)."""
|
|
2381
|
+
# Handle true/false conversion (Rule 17)
|
|
2382
|
+
if expr.name == 'true':
|
|
2383
|
+
return Literal(value=True, literal_type='bool')
|
|
2384
|
+
elif expr.name == 'false':
|
|
2385
|
+
return Literal(value=False, literal_type='bool')
|
|
2386
|
+
|
|
2387
|
+
return Identifier(name=expr.name)
|
|
2388
|
+
|
|
2389
|
+
def _transform_literal(self, expr: Literal) -> Literal:
|
|
2390
|
+
"""Transform literal (Rule 17: true/false to True/False)."""
|
|
2391
|
+
if expr.literal_type == 'bool':
|
|
2392
|
+
# Ensure Python boolean values
|
|
2393
|
+
return Literal(value=bool(expr.value), literal_type='bool')
|
|
2394
|
+
return expr
|
|
2395
|
+
|
|
2396
|
+
def _na_initialiser(self, converted_type: Optional[str]) -> Expression:
|
|
2397
|
+
"""The value of an `= na` initialiser: `na(T)` for a known type, else bare `na`.
|
|
2398
|
+
|
|
2399
|
+
Pine's na is typeless until a later assignment gives it one; PyneCore
|
|
2400
|
+
has the same value (bare `na` -> `lib._na_none`), which the transpiler
|
|
2401
|
+
already emits for `color(na)` and for-loop expression targets. Emitting
|
|
2402
|
+
`na(Any)` would reference a name nothing imports.
|
|
2403
|
+
"""
|
|
2404
|
+
if not converted_type or converted_type == 'Any':
|
|
2405
|
+
return NaLiteral()
|
|
2406
|
+
return FunctionCall(func='na', args=[Identifier(name=converted_type)], kwargs={})
|
|
2407
|
+
|
|
2408
|
+
def _drawing_type_cast(self, cast_name: str, argument: Expression) -> Expression:
|
|
2409
|
+
"""`box(na)` -> `NA(Box)`, an na of the declared type; `box(x)` -> `x` (card #8).
|
|
2410
|
+
Emitted as is, the cast would call the namespace module."""
|
|
2411
|
+
if not isinstance(argument, NaLiteral):
|
|
2412
|
+
return self._transform_expression(argument)
|
|
2413
|
+
type_name = get_type_name(cast_name)
|
|
2414
|
+
self.import_resolver.add_drawing_type(type_name)
|
|
2415
|
+
return FunctionCall(func='NA', args=[Identifier(name=type_name)], kwargs={})
|
|
2416
|
+
|
|
2417
|
+
def _transform_na_literal(self, expr: NaLiteral) -> Identifier:
|
|
2418
|
+
"""Transform na literal to na identifier (Rule 19)."""
|
|
2419
|
+
# In PyneCore, 'na' is used directly, not NA()
|
|
2420
|
+
return Identifier(name='na')
|
|
2421
|
+
|
|
2422
|
+
def _extract_last_expression_from_block(self, block):
|
|
2423
|
+
"""Extract the last expression from a block of statements.
|
|
2424
|
+
In Pine Script, the last evaluated expression in a block is the return value.
|
|
2425
|
+
"""
|
|
2426
|
+
if isinstance(block, Expression):
|
|
2427
|
+
return self._transform_expression(block)
|
|
2428
|
+
last = block[-1] if isinstance(block, list) and block else block
|
|
2429
|
+
# If the last item is an expression statement (ExpressionStatement or raw Expression)
|
|
2430
|
+
if isinstance(last, Expression):
|
|
2431
|
+
return self._transform_expression(last)
|
|
2432
|
+
# lower_block_values rewrites every block it can; emitting only the tail
|
|
2433
|
+
# here would drop the block's statements (card #41) or the binding of a
|
|
2434
|
+
# sole-binding branch, and a bare loop or an empty branch would be
|
|
2435
|
+
# emitted as None (card #50)
|
|
2436
|
+
raise UnsupportedFeatureError(
|
|
2437
|
+
f'{type(last).__name__} as the value of an if/switch expression branch', getattr(last, 'line', None))
|
|
2438
|
+
|
|
2439
|
+
def _transform_if_expression(self, expr: IfExpression) -> TernaryOp:
|
|
2440
|
+
"""Transform if expression to Python inline if/else (Rule 16)."""
|
|
2441
|
+
# if cond\n val1\nelse\n val2 => val1 if cond else val2
|
|
2442
|
+
condition = self._transform_expression(expr.condition)
|
|
2443
|
+
|
|
2444
|
+
# Handle single expression vs block
|
|
2445
|
+
true_expr = self._extract_last_expression_from_block(expr.true_expr)
|
|
2446
|
+
|
|
2447
|
+
if expr.false_expr:
|
|
2448
|
+
false_expr = self._extract_last_expression_from_block(expr.false_expr)
|
|
2449
|
+
elif expr.value_is_bool:
|
|
2450
|
+
# Pine v6: an untaken if of bool type is false, not na (cards #50, #56)
|
|
2451
|
+
false_expr = Literal(value=False, literal_type='bool')
|
|
2452
|
+
else:
|
|
2453
|
+
false_expr = self._untaken_na()
|
|
2454
|
+
|
|
2455
|
+
return TernaryOp(condition=condition, true_expr=true_expr, false_expr=false_expr)
|
|
2456
|
+
|
|
2457
|
+
def _untaken_na(self) -> NaLiteral:
|
|
2458
|
+
"""The value of an if/switch expression whose arms do not run: Pine's na.
|
|
2459
|
+
Python's None is not na to pynecore (`na(None)` is falsy) (card #50)."""
|
|
2460
|
+
self.import_resolver.uses_na = True
|
|
2461
|
+
return NaLiteral()
|
|
2462
|
+
|
|
2463
|
+
def _transform_switch_expression(self, expr: SwitchExpression) -> Expression:
|
|
2464
|
+
"""Transform switch expression to nested ternary (if-elif-else chain)."""
|
|
2465
|
+
# Transform the switch expression to a chain of ternary operators
|
|
2466
|
+
# switch expr
|
|
2467
|
+
# val1 => result1
|
|
2468
|
+
# val2 => result2
|
|
2469
|
+
# => default
|
|
2470
|
+
# Becomes: result1 if (expr == val1) else (result2 if (expr == val2) else default)
|
|
2471
|
+
|
|
2472
|
+
# Or for switch without expr (condition-based):
|
|
2473
|
+
# switch
|
|
2474
|
+
# cond1 => result1
|
|
2475
|
+
# cond2 => result2
|
|
2476
|
+
# => default
|
|
2477
|
+
# Becomes: result1 if cond1 else (result2 if cond2 else default)
|
|
2478
|
+
|
|
2479
|
+
if not expr.cases:
|
|
2480
|
+
# No cases, just return default
|
|
2481
|
+
if expr.default:
|
|
2482
|
+
return self._extract_last_expression_from_block(expr.default)
|
|
2483
|
+
return self._untaken_na()
|
|
2484
|
+
|
|
2485
|
+
# Conditions in case order: the first one binds the subject (card #43)
|
|
2486
|
+
conditions = self._switch_case_conditions(expr.expr, expr.cases)
|
|
2487
|
+
|
|
2488
|
+
# Build nested ternary from right to left
|
|
2489
|
+
result = None
|
|
2490
|
+
if expr.default:
|
|
2491
|
+
result = self._extract_last_expression_from_block(expr.default)
|
|
2492
|
+
elif expr.value_is_bool:
|
|
2493
|
+
# Pine v6: an untaken switch of bool type is false, not na (cards #50, #56)
|
|
2494
|
+
result = Literal(value=False, literal_type='bool')
|
|
2495
|
+
else:
|
|
2496
|
+
result = self._untaken_na()
|
|
2497
|
+
|
|
2498
|
+
# Process cases in reverse order to build nested ternary
|
|
2499
|
+
for condition, (_, case_value) in reversed(list(zip(conditions, expr.cases))):
|
|
2500
|
+
# Transform case value
|
|
2501
|
+
value_expr = self._extract_last_expression_from_block(case_value)
|
|
2502
|
+
|
|
2503
|
+
# Build ternary: value if condition else result
|
|
2504
|
+
result = TernaryOp(
|
|
2505
|
+
condition=condition,
|
|
2506
|
+
true_expr=value_expr,
|
|
2507
|
+
false_expr=result
|
|
2508
|
+
)
|
|
2509
|
+
|
|
2510
|
+
return result
|