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
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Import resolver for PyneCore transpiled code.
|
|
3
|
+
|
|
4
|
+
Collects all used symbols and generates appropriate import statements.
|
|
5
|
+
"""
|
|
6
|
+
import keyword
|
|
7
|
+
from typing import Set, List
|
|
8
|
+
from .ast_nodes import *
|
|
9
|
+
from .errors import UnsupportedFeatureError
|
|
10
|
+
from .ast_walk import walk
|
|
11
|
+
from .block_value_lowering import bool_is_never_na
|
|
12
|
+
from .udt_field_defaults import FIELD_FACTORY_IMPORT, emitted_default_requires_na, is_dotted_default, is_series_default
|
|
13
|
+
from .pine_builtins import (
|
|
14
|
+
PYNECORE_LIB_MODULES, DRAWING_TYPES, CHART_TYPES,
|
|
15
|
+
SERIES_VARIABLES, is_series_variable, needs_plot_style_remap
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ImportResolver:
|
|
20
|
+
"""Collects used symbols and generates import statements."""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.uses_series = False
|
|
24
|
+
self.uses_persistent = False
|
|
25
|
+
self.uses_persistent_series = False
|
|
26
|
+
self.uses_na = False
|
|
27
|
+
self.bool_is_never_na = True # Pine v6 and later; set per script by analyze() (card #76)
|
|
28
|
+
self.uses_any = False
|
|
29
|
+
self.uses_udt = False # Custom type definitions
|
|
30
|
+
self.uses_udt_copy = False # udt_copy for UDT field copying
|
|
31
|
+
self.uses_field_factory = False # a UDT field default read per instance (card #17)
|
|
32
|
+
self.uses_method = False # User-defined methods
|
|
33
|
+
self.uses_method_call = False # method_call for explicit method calling
|
|
34
|
+
self.uses_overload = False # pynecore @overload for same-name declarations
|
|
35
|
+
self.uses_export = False # a library's `export` functions: the Exported proxy and the @export decorator (card #111)
|
|
36
|
+
self.library_imports: List[ImportDecl] = [] # `import user/lib/version as alias`, one Python import each (card #111)
|
|
37
|
+
self.uses_pine_range = False # pine_range for loops
|
|
38
|
+
self.pine_range_name = 'pine_range' # the name pine_range is imported under; the transformer sets an alias when the script uses the name (card #94)
|
|
39
|
+
self.pine_loop_name = '' # the name pine_loop is imported under: a v6 for loop re-reads its end (card #82)
|
|
40
|
+
self.uses_cast_int = False # cast_int for type casting
|
|
41
|
+
self.uses_cast_float = False # cast_float for type casting
|
|
42
|
+
self.uses_cast_bool = False # cast_bool for type casting (Pine v6, card #56)
|
|
43
|
+
# Note: SeriesImpl for function params is handled by runtime SeriesTransformer
|
|
44
|
+
self.lib_modules: Set[str] = set()
|
|
45
|
+
self.builtin_variables: Set[str] = set()
|
|
46
|
+
# Global-scope names the script declares itself (var/varip/assignment). Pine
|
|
47
|
+
# lets a user variable shadow a built-in variable of the same name (they are
|
|
48
|
+
# not reserved keywords), so such a name must NOT be treated/imported as a
|
|
49
|
+
# built-in series — e.g. `var array<bool> hl2 = ...` shadows the `hl2`
|
|
50
|
+
# ((high+low)/2) price source, and importing the built-in would let the
|
|
51
|
+
# runtime overwrite the user's array each bar.
|
|
52
|
+
self.declared_names: Set[str] = set()
|
|
53
|
+
self.drawing_types: Set[str] = set()
|
|
54
|
+
self.chart_types: Set[str] = set()
|
|
55
|
+
self.uses_plot_style = False
|
|
56
|
+
# A namespace spelled by an alias because an input or export keeps its name
|
|
57
|
+
# (scope_renamer, card #9): module name -> alias ('size' -> '_size_module')
|
|
58
|
+
self.aliased_modules: dict[str, str] = {}
|
|
59
|
+
|
|
60
|
+
def analyze(self, script: Script) -> None:
|
|
61
|
+
"""Analyze script to determine what needs to be imported."""
|
|
62
|
+
self.bool_is_never_na = bool_is_never_na(script)
|
|
63
|
+
self.library_imports = list(script.imports)
|
|
64
|
+
# the UDTs and enums the script declares: a UDT-typed field is made per instance; an enum is a StrEnum
|
|
65
|
+
self._udt_names = {decl.name for decl in script.declarations if isinstance(decl, TypeDecl)}
|
|
66
|
+
self.uses_enum = any(isinstance(decl, EnumDecl) for decl in script.declarations)
|
|
67
|
+
# Pre-pass: collect every name the script declares, so a user variable that
|
|
68
|
+
# shadows a built-in (e.g. hl2) is not mistaken for the built-in below.
|
|
69
|
+
self._collect_declared_names(script)
|
|
70
|
+
|
|
71
|
+
# Check for Series usage (var declarations, globals)
|
|
72
|
+
for decl in script.declarations:
|
|
73
|
+
if isinstance(decl, (VarDecl, VaripDecl)):
|
|
74
|
+
self.uses_persistent = True
|
|
75
|
+
elif isinstance(decl, Assignment):
|
|
76
|
+
# Global assignments are Series
|
|
77
|
+
self.uses_series = True
|
|
78
|
+
|
|
79
|
+
# Check body for Series usage
|
|
80
|
+
if script.body:
|
|
81
|
+
self.uses_series = True
|
|
82
|
+
|
|
83
|
+
# Scan every node once; the shared walk also enters keyword arguments
|
|
84
|
+
# and else-if arms (card #33: a nested call was rescanned per level)
|
|
85
|
+
for node in walk(script):
|
|
86
|
+
self._scan_node(node)
|
|
87
|
+
|
|
88
|
+
def _collect_declared_names(self, node: Any) -> None:
|
|
89
|
+
"""Recursively collect GLOBAL-scope names the script declares (var/varip/
|
|
90
|
+
assignment/input) so they can shadow a same-named built-in variable.
|
|
91
|
+
|
|
92
|
+
Only global-scope declarations shadow a built-in everywhere the built-in
|
|
93
|
+
would be imported. Function parameters and function-local declarations are
|
|
94
|
+
scoped to that function, so they must NOT suppress the global import — a
|
|
95
|
+
script can have a function parameter named ``time`` while still using the
|
|
96
|
+
built-in ``time`` at global scope. We therefore skip ``FuncDecl`` subtrees
|
|
97
|
+
entirely and never collect ``Parameter`` names.
|
|
98
|
+
|
|
99
|
+
Pine declarations use a plain ``str`` name/target; a tuple target's names
|
|
100
|
+
are declarations too (card #89: `[close, unit] = pair()` at the top level).
|
|
101
|
+
|
|
102
|
+
An input is a global too (card #86): pynecore reads the library series for
|
|
103
|
+
every read of an imported name except a function parameter, and a source
|
|
104
|
+
input is emitted as a local of main() (``close: Series[float] = _close_src``).
|
|
105
|
+
A block local named like a built-in never reaches here: the scope renamer
|
|
106
|
+
gives it its own name.
|
|
107
|
+
"""
|
|
108
|
+
if isinstance(node, FuncDecl):
|
|
109
|
+
return # function-scoped names never shadow a global built-in import
|
|
110
|
+
if isinstance(node, (VarDecl, VaripDecl, InputDecl)):
|
|
111
|
+
if isinstance(node.name, str):
|
|
112
|
+
self.declared_names.add(node.name)
|
|
113
|
+
elif isinstance(node, Assignment):
|
|
114
|
+
if isinstance(node.target, str):
|
|
115
|
+
self.declared_names.add(node.target)
|
|
116
|
+
elif isinstance(node.target, TupleDestructure): # `[close, x] = f()` (card #89)
|
|
117
|
+
self.declared_names.update(node.target.names)
|
|
118
|
+
|
|
119
|
+
if isinstance(node, (list, tuple)):
|
|
120
|
+
for item in node:
|
|
121
|
+
self._collect_declared_names(item)
|
|
122
|
+
elif hasattr(node, '__dict__'):
|
|
123
|
+
for attr_value in node.__dict__.values():
|
|
124
|
+
if isinstance(attr_value, (ASTNode, list, tuple, dict)):
|
|
125
|
+
self._collect_declared_names(attr_value)
|
|
126
|
+
|
|
127
|
+
def _scan_node(self, node: Any) -> None:
|
|
128
|
+
"""Record the symbols of one AST node (not of its children)."""
|
|
129
|
+
# Handle NaLiteral
|
|
130
|
+
if isinstance(node, NaLiteral):
|
|
131
|
+
self.uses_na = True
|
|
132
|
+
|
|
133
|
+
# Handle function calls
|
|
134
|
+
if isinstance(node, FunctionCall):
|
|
135
|
+
self._process_function_call(node)
|
|
136
|
+
|
|
137
|
+
# Handle member access
|
|
138
|
+
if isinstance(node, MemberAccess):
|
|
139
|
+
self._process_member_access(node)
|
|
140
|
+
|
|
141
|
+
# Handle identifiers
|
|
142
|
+
if isinstance(node, Identifier):
|
|
143
|
+
self._process_identifier(node.name)
|
|
144
|
+
|
|
145
|
+
# Handle script declaration
|
|
146
|
+
if isinstance(node, (IndicatorDecl, StrategyDecl, LibraryDecl)):
|
|
147
|
+
self.lib_modules.add('script')
|
|
148
|
+
|
|
149
|
+
# A library's exported function (card #111)
|
|
150
|
+
if isinstance(node, FuncDecl) and node.is_export:
|
|
151
|
+
self.uses_export = True
|
|
152
|
+
|
|
153
|
+
# Handle input declarations
|
|
154
|
+
if isinstance(node, InputDecl):
|
|
155
|
+
self.lib_modules.add('input')
|
|
156
|
+
|
|
157
|
+
# Handle var/varip declarations (including inside blocks)
|
|
158
|
+
if isinstance(node, (VarDecl, VaripDecl)):
|
|
159
|
+
self.uses_persistent = True
|
|
160
|
+
|
|
161
|
+
# Handle type declarations (custom types)
|
|
162
|
+
if isinstance(node, TypeDecl):
|
|
163
|
+
self.uses_udt = True
|
|
164
|
+
# Scan field types for drawing type imports (e.g., lbl: Label)
|
|
165
|
+
from .pine_builtins import get_type_name
|
|
166
|
+
from .transformer import convert_pine_generic_to_python
|
|
167
|
+
for field_info in node.fields:
|
|
168
|
+
if len(field_info) >= 2:
|
|
169
|
+
pine_type = field_info[1] # type_hint string (lowercase from Pine, e.g., "label")
|
|
170
|
+
default_val = field_info[2] if len(field_info) >= 3 else None
|
|
171
|
+
if isinstance(pine_type, str):
|
|
172
|
+
# Convert Pine type to Python type (e.g., "label" → "Label")
|
|
173
|
+
py_type = get_type_name(pine_type)
|
|
174
|
+
# Check if it's a drawing or chart type
|
|
175
|
+
if py_type in DRAWING_TYPES:
|
|
176
|
+
self.drawing_types.add(py_type)
|
|
177
|
+
elif py_type in CHART_TYPES:
|
|
178
|
+
self.chart_types.add(py_type)
|
|
179
|
+
# ... or holds one: the field annotation `matrix<float>` -> `Matrix[float]` (card #8)
|
|
180
|
+
self._add_types_named_in(convert_pine_generic_to_python(pine_type))
|
|
181
|
+
# Codegen emits na or NA(Type) where emitted_default_requires_na says so
|
|
182
|
+
holds_a_udt = pine_type in self._udt_names and (default_val is None
|
|
183
|
+
or isinstance(default_val, NaLiteral))
|
|
184
|
+
if holds_a_udt or is_series_default(default_val) or is_dotted_default(default_val):
|
|
185
|
+
self.uses_field_factory = True
|
|
186
|
+
if emitted_default_requires_na(py_type, default_val, self.bool_is_never_na):
|
|
187
|
+
self.uses_na = True
|
|
188
|
+
|
|
189
|
+
# Handle method declarations
|
|
190
|
+
if isinstance(node, FuncDecl):
|
|
191
|
+
if hasattr(node, 'is_method') and node.is_method:
|
|
192
|
+
self.uses_method = True
|
|
193
|
+
|
|
194
|
+
# Handle for loops (need pine_range)
|
|
195
|
+
if isinstance(node, ForLoop):
|
|
196
|
+
if node.counter_name:
|
|
197
|
+
self.pine_loop_name = node.counter_helper
|
|
198
|
+
else:
|
|
199
|
+
self.uses_pine_range = True
|
|
200
|
+
|
|
201
|
+
# Check for Any in type hints
|
|
202
|
+
if isinstance(node, (Assignment, Reassignment, VarDecl, VaripDecl, Parameter)):
|
|
203
|
+
if hasattr(node, 'type_hint') and node.type_hint:
|
|
204
|
+
type_hint_str = str(node.type_hint)
|
|
205
|
+
if 'Any' in type_hint_str:
|
|
206
|
+
self.uses_any = True
|
|
207
|
+
|
|
208
|
+
# Extract drawing/structural types from type hints like Persistent[Line], Matrix[float], etc.
|
|
209
|
+
self._add_types_named_in(type_hint_str)
|
|
210
|
+
|
|
211
|
+
def _add_types_named_in(self, type_hint: str) -> None:
|
|
212
|
+
"""Import the drawing and chart types a Python type hint names:
|
|
213
|
+
Persistent[Line], Matrix[float], dict[str, Line], ..."""
|
|
214
|
+
import re
|
|
215
|
+
for match in re.finditer(r'\b([A-Z][a-zA-Z]*)\b', type_hint):
|
|
216
|
+
self.add_drawing_type(match.group(1))
|
|
217
|
+
|
|
218
|
+
def _process_function_call(self, call: FunctionCall) -> None:
|
|
219
|
+
"""Process a function call to extract module dependencies."""
|
|
220
|
+
func_name = None
|
|
221
|
+
|
|
222
|
+
if isinstance(call.func, str):
|
|
223
|
+
func_name = call.func
|
|
224
|
+
# Handle dotted function names like "box.new" -> extract "box"
|
|
225
|
+
if '.' in func_name:
|
|
226
|
+
self._add_namespace(func_name.split('.')[0])
|
|
227
|
+
# Handle standalone function names like "plot", "bgcolor", "barcolor"
|
|
228
|
+
elif func_name in PYNECORE_LIB_MODULES:
|
|
229
|
+
self.lib_modules.add(func_name)
|
|
230
|
+
# Handle series variables used as functions (e.g., time(), time_close())
|
|
231
|
+
elif is_series_variable(func_name):
|
|
232
|
+
self.builtin_variables.add(func_name)
|
|
233
|
+
self.uses_series = True
|
|
234
|
+
elif isinstance(call.func, NaLiteral):
|
|
235
|
+
# Special case: na() used as a function call, not a literal
|
|
236
|
+
# Import the na function from pynecore.lib
|
|
237
|
+
self.lib_modules.add('na')
|
|
238
|
+
elif isinstance(call.func, MemberAccess):
|
|
239
|
+
# Extract module name from member access
|
|
240
|
+
if isinstance(call.func.object, str):
|
|
241
|
+
self._add_namespace(call.func.object)
|
|
242
|
+
elif isinstance(call.func.object, Identifier):
|
|
243
|
+
self._add_namespace(call.func.object.name)
|
|
244
|
+
|
|
245
|
+
# Plot style references in kwargs (the walk visits the values themselves)
|
|
246
|
+
for value in call.kwargs.values():
|
|
247
|
+
if isinstance(value, MemberAccess):
|
|
248
|
+
# Handle both Identifier and str object types
|
|
249
|
+
module_name = value.object.name if isinstance(value.object, Identifier) else str(value.object)
|
|
250
|
+
full_name = f"{module_name}.{value.member}"
|
|
251
|
+
if needs_plot_style_remap(full_name):
|
|
252
|
+
self.uses_plot_style = True
|
|
253
|
+
|
|
254
|
+
def _process_member_access(self, member: MemberAccess) -> None:
|
|
255
|
+
"""Process member access to extract module dependencies."""
|
|
256
|
+
if isinstance(member.object, str):
|
|
257
|
+
module = member.object
|
|
258
|
+
self._add_namespace(module)
|
|
259
|
+
|
|
260
|
+
# Check for plot style
|
|
261
|
+
full_name = f"{module}.{member.member}"
|
|
262
|
+
if needs_plot_style_remap(full_name):
|
|
263
|
+
self.uses_plot_style = True
|
|
264
|
+
|
|
265
|
+
elif isinstance(member.object, Identifier):
|
|
266
|
+
module = member.object.name
|
|
267
|
+
self._add_namespace(module)
|
|
268
|
+
|
|
269
|
+
# Check for plot style
|
|
270
|
+
full_name = f"{module}.{member.member}"
|
|
271
|
+
if needs_plot_style_remap(full_name):
|
|
272
|
+
self.uses_plot_style = True
|
|
273
|
+
|
|
274
|
+
def _process_identifier(self, name: str) -> None:
|
|
275
|
+
"""Process identifier to check for built-in variables and module constants."""
|
|
276
|
+
# Check for dotted identifiers like "xloc.bar_time" or "line.style_dashed"
|
|
277
|
+
if '.' in name:
|
|
278
|
+
self._add_namespace(name.split('.')[0])
|
|
279
|
+
# Check for built-in Series variables — but a name the script declares
|
|
280
|
+
# itself shadows the built-in, so it must not be imported as one.
|
|
281
|
+
elif is_series_variable(name) and name not in self.declared_names:
|
|
282
|
+
self.builtin_variables.add(name)
|
|
283
|
+
self.uses_series = True
|
|
284
|
+
|
|
285
|
+
def _add_namespace(self, module: str) -> None:
|
|
286
|
+
"""Import what a dotted name starts with: a lib namespace (Pine's `str`
|
|
287
|
+
is pynecore's `string`), or a built-in series used as a method receiver
|
|
288
|
+
(`close.isUp()`). An alias head is imported by generate_imports."""
|
|
289
|
+
if module in PYNECORE_LIB_MODULES:
|
|
290
|
+
self.lib_modules.add(module)
|
|
291
|
+
elif module == 'str':
|
|
292
|
+
self.lib_modules.add('string')
|
|
293
|
+
elif is_series_variable(module) and module not in self.declared_names:
|
|
294
|
+
self.builtin_variables.add(module)
|
|
295
|
+
self.uses_series = True
|
|
296
|
+
|
|
297
|
+
def add_drawing_type(self, type_name: str) -> None:
|
|
298
|
+
"""Manually add a drawing type that was detected."""
|
|
299
|
+
if type_name in DRAWING_TYPES:
|
|
300
|
+
self.drawing_types.add(type_name)
|
|
301
|
+
elif type_name in CHART_TYPES:
|
|
302
|
+
self.chart_types.add(type_name)
|
|
303
|
+
|
|
304
|
+
def add_module(self, module_name: str) -> None:
|
|
305
|
+
"""Manually add a module dependency."""
|
|
306
|
+
if module_name in PYNECORE_LIB_MODULES:
|
|
307
|
+
self.lib_modules.add(module_name)
|
|
308
|
+
|
|
309
|
+
#: built-in namespace alias of a library -> the name the library module is imported under
|
|
310
|
+
shadowed_namespaces: dict = None
|
|
311
|
+
|
|
312
|
+
def library_import_lines(self) -> List[str]:
|
|
313
|
+
"""`import user/lib/version as alias` is the Python import of the module `lib` under `alias` (card #111).
|
|
314
|
+
|
|
315
|
+
pynecore loads a library as a module (`<lib>.py`, on `sys.path`); the user and the version are not part of
|
|
316
|
+
the module name, so one library name stands for one library: two imports of it that differ in user or
|
|
317
|
+
version are refused, as is a name Python cannot import (a keyword) or an alias (the library name when none is
|
|
318
|
+
given) that would shadow a pynecore.lib name the script reads. (An alias spelled like `lib` or a helper is
|
|
319
|
+
renamed before this point, by the scope renamer.)"""
|
|
320
|
+
lines: List[str] = []
|
|
321
|
+
libraries: dict = {}
|
|
322
|
+
self.shadowed_namespaces = {}
|
|
323
|
+
for imported in self.library_imports:
|
|
324
|
+
module = imported.library
|
|
325
|
+
if keyword.iskeyword(module) or not module.isidentifier():
|
|
326
|
+
raise UnsupportedFeatureError(
|
|
327
|
+
f'import {imported.user}/{module}/{imported.version}: "{module}" is not a Python module name',
|
|
328
|
+
imported.line)
|
|
329
|
+
identity = (imported.user, imported.version)
|
|
330
|
+
if libraries.setdefault(module, identity) != identity:
|
|
331
|
+
raise UnsupportedFeatureError(
|
|
332
|
+
f'two imports of the library "{module}" differ in user or version: the module is the library '
|
|
333
|
+
f'name, so one name stands for one library', imported.line)
|
|
334
|
+
alias = imported.alias or module
|
|
335
|
+
if keyword.iskeyword(alias):
|
|
336
|
+
raise UnsupportedFeatureError(
|
|
337
|
+
f'import {imported.user}/{module}/{imported.version} as {alias}: "{alias}" is a Python keyword',
|
|
338
|
+
imported.line)
|
|
339
|
+
# PyneComp's spelling: `import lib.<user>.<library>.v<version> as <alias>`
|
|
340
|
+
path = f"lib.{str(imported.user).replace('-', '_')}.{module}"
|
|
341
|
+
if imported.version not in (None, ''):
|
|
342
|
+
path += f'.v{imported.version}'
|
|
343
|
+
if alias in self.lib_modules | self.builtin_variables | PYNECORE_LIB_MODULES:
|
|
344
|
+
# an alias that is a built-in namespace (`import TradingView/ta/12` as `ta`): the module is
|
|
345
|
+
# imported aside and `ta__ren__ = shadowed_namespace(ta__ren____lib, ta)` reads the library
|
|
346
|
+
# first and the built-in after; every `ta.` of the script goes through it
|
|
347
|
+
self.shadowed_namespaces[alias] = f'{alias}__ren____lib'
|
|
348
|
+
line = f'import {path} as {alias}__ren____lib'
|
|
349
|
+
else:
|
|
350
|
+
line = f'import {path} as {alias}'
|
|
351
|
+
if line not in lines:
|
|
352
|
+
lines.append(line)
|
|
353
|
+
return lines
|
|
354
|
+
|
|
355
|
+
def generate_imports(self) -> List[str]:
|
|
356
|
+
"""Generate import statements in the correct order."""
|
|
357
|
+
imports = self.library_import_lines()
|
|
358
|
+
|
|
359
|
+
# 0. Typing imports (if used)
|
|
360
|
+
if self.uses_any:
|
|
361
|
+
imports.append("from typing import Any")
|
|
362
|
+
|
|
363
|
+
# 0b. A UDT field default read per instance: field(default_factory=...)
|
|
364
|
+
if self.uses_field_factory:
|
|
365
|
+
imports.append(FIELD_FACTORY_IMPORT)
|
|
366
|
+
if getattr(self, 'uses_enum', False):
|
|
367
|
+
imports.append('from enum import StrEnum as Enum')
|
|
368
|
+
|
|
369
|
+
# 1. Pine range and the re-reading loop counter (if used)
|
|
370
|
+
if self.pine_loop_name == 'pine_loop':
|
|
371
|
+
imports.append("from pynecore import pine_loop")
|
|
372
|
+
elif self.pine_loop_name:
|
|
373
|
+
imports.append(f"from pynecore import pine_loop as {self.pine_loop_name}")
|
|
374
|
+
if self.uses_pine_range and self.pine_range_name == 'pine_range':
|
|
375
|
+
imports.append("from pynecore import pine_range")
|
|
376
|
+
elif self.uses_pine_range:
|
|
377
|
+
imports.append(f"from pynecore import pine_range as {self.pine_range_name}")
|
|
378
|
+
|
|
379
|
+
# 2. Pine cast (if used)
|
|
380
|
+
if self.uses_cast_int or self.uses_cast_float or self.uses_cast_bool:
|
|
381
|
+
cast_funcs = []
|
|
382
|
+
if self.uses_cast_bool:
|
|
383
|
+
cast_funcs.append('cast_bool')
|
|
384
|
+
if self.uses_cast_float:
|
|
385
|
+
cast_funcs.append('cast_float')
|
|
386
|
+
if self.uses_cast_int:
|
|
387
|
+
cast_funcs.append('cast_int')
|
|
388
|
+
imports.append(f"from pynecore.core.pine_cast import {', '.join(sorted(cast_funcs))}")
|
|
389
|
+
|
|
390
|
+
# 3. Pine method (only import what's used)
|
|
391
|
+
if self.uses_method or self.uses_method_call:
|
|
392
|
+
method_imports = []
|
|
393
|
+
if self.uses_method:
|
|
394
|
+
method_imports.append('method')
|
|
395
|
+
if self.uses_method_call:
|
|
396
|
+
method_imports.append('method_call')
|
|
397
|
+
imports.append(f"from pynecore.core.pine_method import {', '.join(method_imports)}")
|
|
398
|
+
|
|
399
|
+
# 3a. A library's export surface (card #111)
|
|
400
|
+
if self.uses_export:
|
|
401
|
+
imports.append("from pynecore.core.pine_export import Exported, export")
|
|
402
|
+
|
|
403
|
+
# 3b. Overload dispatcher for same-name declarations (card #39)
|
|
404
|
+
if self.uses_overload:
|
|
405
|
+
imports.append("from pynecore.core.overload import overload")
|
|
406
|
+
|
|
407
|
+
# 4. Pine UDT (only import what's used)
|
|
408
|
+
if self.uses_udt or self.uses_udt_copy:
|
|
409
|
+
udt_imports = []
|
|
410
|
+
if self.uses_udt:
|
|
411
|
+
udt_imports.append('udt')
|
|
412
|
+
if self.uses_udt_copy:
|
|
413
|
+
udt_imports.append('udt_copy')
|
|
414
|
+
imports.append(f"from pynecore.core.pine_udt import {', '.join(udt_imports)}")
|
|
415
|
+
|
|
416
|
+
# 5. PyneCore core types - Series only (Persistent moved to pynecore.types)
|
|
417
|
+
# Note: Series import is now handled in types import section with Persistent
|
|
418
|
+
|
|
419
|
+
# 6. Main library imports + built-in variables (combined into multi-line statement)
|
|
420
|
+
# Ensure na is imported when used as bare identifier (e.g., f = na)
|
|
421
|
+
if self.uses_na:
|
|
422
|
+
self.lib_modules.add('na')
|
|
423
|
+
self.lib_modules.add('NA')
|
|
424
|
+
|
|
425
|
+
all_lib_imports = []
|
|
426
|
+
if self.lib_modules:
|
|
427
|
+
all_lib_imports.extend(sorted(self.lib_modules))
|
|
428
|
+
if self.builtin_variables:
|
|
429
|
+
all_lib_imports.extend(sorted(self.builtin_variables))
|
|
430
|
+
|
|
431
|
+
# An aliased namespace follows the plain names (`size as _size_module`)
|
|
432
|
+
aliased_imports = [f"{module} as {alias}" for module, alias in sorted(self.aliased_modules.items())]
|
|
433
|
+
|
|
434
|
+
if all_lib_imports or aliased_imports:
|
|
435
|
+
combined_imports = sorted(set(all_lib_imports)) + aliased_imports
|
|
436
|
+
|
|
437
|
+
# Generate multi-line import statement like ground truth
|
|
438
|
+
# Format: from pynecore.lib import (\n item1, item2, item3,\n item4, item5\n)
|
|
439
|
+
import_str = "from pynecore.lib import (\n "
|
|
440
|
+
import_str += ', '.join(combined_imports)
|
|
441
|
+
import_str += "\n)"
|
|
442
|
+
imports.append(import_str)
|
|
443
|
+
|
|
444
|
+
# 8. Plot styles (if used)
|
|
445
|
+
if self.uses_plot_style:
|
|
446
|
+
imports.append("from pynecore.lib import plot_style")
|
|
447
|
+
|
|
448
|
+
# 9. Types from pynecore.types (drawing types + Persistent + Series + Matrix if used)
|
|
449
|
+
types_import = []
|
|
450
|
+
if self.drawing_types:
|
|
451
|
+
types_import.extend(sorted(self.drawing_types))
|
|
452
|
+
if self.uses_persistent:
|
|
453
|
+
types_import.append('Persistent')
|
|
454
|
+
if self.uses_persistent_series:
|
|
455
|
+
types_import.append('PersistentSeries')
|
|
456
|
+
if self.uses_series:
|
|
457
|
+
types_import.append('Series')
|
|
458
|
+
# Matrix is always imported if we have drawing types or persistent (it's a common type)
|
|
459
|
+
# Actually, only import if explicitly used
|
|
460
|
+
|
|
461
|
+
if types_import:
|
|
462
|
+
imports.append(f"from pynecore.types import {', '.join(sorted(set(types_import)))}")
|
|
463
|
+
|
|
464
|
+
# 10. Chart types (if used)
|
|
465
|
+
if self.chart_types:
|
|
466
|
+
types = sorted(self.chart_types)
|
|
467
|
+
imports.append(f"from pynecore.types.chart import {', '.join(types)}")
|
|
468
|
+
|
|
469
|
+
return imports
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Every top-level input becomes a main() parameter, as PyneComp compiles it.
|
|
2
|
+
|
|
3
|
+
PyneComp numbers the input calls of a script in source order. An input that
|
|
4
|
+
is the whole value of a top-level assignment (``len = input(14)``) becomes the
|
|
5
|
+
parameter of that name; one inside an expression (``w = input.float(80) / 100``)
|
|
6
|
+
becomes the parameter ``__input_<n>__`` (n: its 0-based number among all
|
|
7
|
+
inputs) and the expression reads that name; the first one of a ``var x = ...``
|
|
8
|
+
declaration is ``x__input``.
|
|
9
|
+
|
|
10
|
+
A Pine v4 input is typed as TradingView's v4-to-v5 conversion types it (see
|
|
11
|
+
:func:`typed_input_function`).
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import dataclasses
|
|
16
|
+
from typing import List
|
|
17
|
+
|
|
18
|
+
from .ast_nodes import (
|
|
19
|
+
Assignment, EnumDecl, FuncDecl, FunctionCall, Identifier, InputDecl, Literal, MemberAccess, Script, TypeDecl,
|
|
20
|
+
UnaryOp,
|
|
21
|
+
)
|
|
22
|
+
from .ast_walk import is_node, map_children, walk
|
|
23
|
+
|
|
24
|
+
#: keywords the generic Pine v4 ``input()`` is called with and keeps
|
|
25
|
+
_GENERIC_KEYWORDS = frozenset({'title', 'defval', 'type'})
|
|
26
|
+
_KIND_FUNCTIONS = {'integer': 'input.int', 'float': 'input.float', 'bool': 'input.bool',
|
|
27
|
+
'string': 'input.string', 'color': 'input.color', 'source': 'input.source', 'time': 'input.time'}
|
|
28
|
+
#: Pine v4 type constants passed positionally as input()'s third argument
|
|
29
|
+
_LEGACY_TYPE_CONSTANTS = {'input.integer': 'input.int', 'input.float': 'input.float', 'input.bool': 'input.bool',
|
|
30
|
+
'input.string': 'input.string', 'input.source': 'input.source', 'input.color': 'input.color',
|
|
31
|
+
'input.resolution': 'input.timeframe', 'input.symbol': 'input.symbol',
|
|
32
|
+
'input.session': 'input.session', 'input.time': 'input.time'}
|
|
33
|
+
_FUNCTION_KINDS = {function: kind for kind, function in _KIND_FUNCTIONS.items()}
|
|
34
|
+
_SOURCES = frozenset({'close', 'open', 'high', 'low', 'volume', 'hl2', 'hlc3', 'ohlc4', 'hlcc4'})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_input_call(node) -> bool:
|
|
38
|
+
return isinstance(node, FunctionCall) and isinstance(node.func, str) and (
|
|
39
|
+
node.func == 'input' or node.func.startswith('input.'))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _default_value(call: FunctionCall):
|
|
43
|
+
return call.args[0] if call.args else call.kwargs.get('defval')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def default_kind(value, constants: dict | None = None) -> str | None:
|
|
47
|
+
"""The input kind a default value has on its own: what the generic input() infers.
|
|
48
|
+
``constants``: top-level names bound to a literal, by the literal's kind."""
|
|
49
|
+
if isinstance(value, UnaryOp) and value.op == '-':
|
|
50
|
+
value = value.operand
|
|
51
|
+
if isinstance(value, Identifier) and constants and value.name in constants:
|
|
52
|
+
return constants[value.name]
|
|
53
|
+
if isinstance(value, FunctionCall) and value.func == 'timestamp':
|
|
54
|
+
return 'time'
|
|
55
|
+
if isinstance(value, Literal):
|
|
56
|
+
return {'int': 'integer', 'float': 'float', 'bool': 'bool', 'string': 'string',
|
|
57
|
+
'color': 'color'}.get(value.literal_type)
|
|
58
|
+
if isinstance(value, Identifier):
|
|
59
|
+
if value.name in _SOURCES:
|
|
60
|
+
return 'source'
|
|
61
|
+
if value.name.startswith('color.'):
|
|
62
|
+
return 'color'
|
|
63
|
+
if isinstance(value, FunctionCall) and isinstance(value.func, str) and value.func.startswith('color.'):
|
|
64
|
+
return 'color'
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def typed_input_function(call: FunctionCall, version: int = 6, constants: dict | None = None) -> str:
|
|
69
|
+
"""The input function PyneComp calls for ``call`` (a Pine v4 rule: TradingView's
|
|
70
|
+
v4-to-v5 conversion). A v4 input stays the generic ``input()`` when it passes
|
|
71
|
+
nothing but title and default and its default already has its kind; any other
|
|
72
|
+
keyword (group, inline, minval, options, ...) makes it ``input.<kind>``. Pine v5
|
|
73
|
+
and later keep the function the source calls."""
|
|
74
|
+
if version > 4:
|
|
75
|
+
return call.func
|
|
76
|
+
extra = (set(call.kwargs) - _GENERIC_KEYWORDS) or len(call.args) > 2
|
|
77
|
+
natural = default_kind(_default_value(call), constants)
|
|
78
|
+
if call.func == 'input':
|
|
79
|
+
if not extra:
|
|
80
|
+
return 'input'
|
|
81
|
+
if natural == 'integer' and any(isinstance(call.kwargs.get(key), Literal)
|
|
82
|
+
and call.kwargs[key].literal_type == 'float'
|
|
83
|
+
for key in ('minval', 'maxval', 'step')):
|
|
84
|
+
return 'input.float' # a fractional range makes an integer default a float input
|
|
85
|
+
return _KIND_FUNCTIONS.get(natural, 'input')
|
|
86
|
+
if not extra and _FUNCTION_KINDS.get(call.func) == natural:
|
|
87
|
+
return 'input' # `type=input.integer` with an integer default adds nothing
|
|
88
|
+
return call.func
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _type_constant(value) -> str | None:
|
|
92
|
+
name = value.name if isinstance(value, Identifier) else (
|
|
93
|
+
f'{value.object}.{value.member}' if isinstance(value, MemberAccess) and isinstance(value.object, str) else None)
|
|
94
|
+
return _LEGACY_TYPE_CONSTANTS.get(name) if name else None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _declaration(name: str, call: FunctionCall, line: int, version: int, constants: dict) -> InputDecl:
|
|
98
|
+
# a `type=` the parser could not map to an input.* function (a variable) is dropped, and so
|
|
99
|
+
# is an `active=` that is not a constant: it only greys the field out (PyneComp keeps a literal)
|
|
100
|
+
kwargs = {key: value for key, value in call.kwargs.items()
|
|
101
|
+
if key != 'type' and not (key == 'active' and not isinstance(value, Literal))}
|
|
102
|
+
args = list(call.args)
|
|
103
|
+
# Pine v4 `input(defval, title, input.integer, ...)`: the third positional is the type
|
|
104
|
+
if call.func == 'input' and len(args) >= 3 and _type_constant(args[2]):
|
|
105
|
+
call = dataclasses.replace(call, func=_type_constant(args[2]))
|
|
106
|
+
args = args[:2] + args[3:]
|
|
107
|
+
function = typed_input_function(dataclasses.replace(call, args=args, kwargs=kwargs), version, constants)
|
|
108
|
+
if function == 'input' and len(args) > 2:
|
|
109
|
+
# the generic input(defval, title, tooltip, inline, group): the rest go by name
|
|
110
|
+
named = dict(zip(('tooltip', 'inline', 'group'), args[2:]))
|
|
111
|
+
args, kwargs = args[:2], {**named, **kwargs}
|
|
112
|
+
return InputDecl(name=name, func=function, args=args,
|
|
113
|
+
kwargs=kwargs, line=line, column=getattr(call, 'column', 0))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _literal_constants(items) -> dict:
|
|
117
|
+
"""Top-level names bound once to a literal -> the literal's input kind."""
|
|
118
|
+
kinds: dict = {}
|
|
119
|
+
for item in items:
|
|
120
|
+
if isinstance(item, Assignment) and isinstance(item.target, str) and isinstance(item.value, Literal):
|
|
121
|
+
kind = default_kind(item.value)
|
|
122
|
+
if kind:
|
|
123
|
+
kinds[item.target] = kind
|
|
124
|
+
return kinds
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _replace_inputs(node, names: dict):
|
|
128
|
+
"""``node`` with each input call in ``names`` (by id) read as its parameter."""
|
|
129
|
+
if id(node) in names:
|
|
130
|
+
return Identifier(name=names[id(node)], line=getattr(node, 'line', 0), column=getattr(node, 'column', 0))
|
|
131
|
+
if not is_node(node):
|
|
132
|
+
return node
|
|
133
|
+
changes = {field.name: map_children(getattr(node, field.name), lambda child: _replace_inputs(child, names))
|
|
134
|
+
for field in dataclasses.fields(node) if field.name not in ('line', 'column')}
|
|
135
|
+
return dataclasses.replace(node, **changes)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def hoist_inputs(script: Script) -> Script:
|
|
139
|
+
"""``script`` with every top-level input call made an InputDecl, in source order."""
|
|
140
|
+
items = sorted(list(script.declarations) + list(script.body), key=lambda item: getattr(item, 'line', 0))
|
|
141
|
+
body_ids = {id(item) for item in script.body}
|
|
142
|
+
version_text = getattr(script.version, 'version', '') if script.version else ''
|
|
143
|
+
version = int(version_text) if str(version_text).isdigit() else 6
|
|
144
|
+
constants = _literal_constants(items)
|
|
145
|
+
declarations: List = []
|
|
146
|
+
body: List = []
|
|
147
|
+
number = 0
|
|
148
|
+
for item in items:
|
|
149
|
+
if isinstance(item, InputDecl):
|
|
150
|
+
call = FunctionCall(func=item.func, args=item.args, kwargs=item.kwargs)
|
|
151
|
+
hoisted = _declaration(item.name, call, item.line, version, constants)
|
|
152
|
+
declarations.append(dataclasses.replace(item, func=hoisted.func, args=hoisted.args, kwargs=hoisted.kwargs))
|
|
153
|
+
number += 1
|
|
154
|
+
continue
|
|
155
|
+
if isinstance(item, (FuncDecl, TypeDecl, EnumDecl)):
|
|
156
|
+
(body if id(item) in body_ids else declarations).append(item)
|
|
157
|
+
continue
|
|
158
|
+
if isinstance(item, Assignment) and isinstance(item.target, str) and is_input_call(item.value):
|
|
159
|
+
declarations.append(_declaration(item.target, item.value, item.line, version, constants))
|
|
160
|
+
number += 1
|
|
161
|
+
continue
|
|
162
|
+
calls = [node for node in walk(_expressions_of(item)) if is_input_call(node)]
|
|
163
|
+
names = {}
|
|
164
|
+
from .ast_nodes import VarDecl, VaripDecl
|
|
165
|
+
for call in calls:
|
|
166
|
+
# `var x = input(...)` (or a method of it) reads the parameter `x__input` (PyneComp, 33 in the reference)
|
|
167
|
+
name = (f'{item.name}__input' if isinstance(item, (VarDecl, VaripDecl)) and call is _chain_head(item.value)
|
|
168
|
+
else f'__input_{number}__')
|
|
169
|
+
names[id(call)] = name
|
|
170
|
+
declarations.append(_declaration(name, call, item.line, version, constants))
|
|
171
|
+
number += 1
|
|
172
|
+
rebuilt = _replace_inputs(item, names) if names else item
|
|
173
|
+
(body if id(item) in body_ids else declarations).append(rebuilt)
|
|
174
|
+
declarations.sort(key=lambda item: getattr(item, 'line', 0))
|
|
175
|
+
return dataclasses.replace(script, declarations=declarations, body=body)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _chain_head(value):
|
|
179
|
+
"""The value a method chain starts from: `input.enum(...)` of `input.enum(...).to_string()`."""
|
|
180
|
+
from .ast_nodes import MethodCall
|
|
181
|
+
while isinstance(value, MethodCall):
|
|
182
|
+
value = value.object
|
|
183
|
+
return value
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _expressions_of(item):
|
|
187
|
+
"""The parts of a top-level statement evaluated at the top level (not a nested block)."""
|
|
188
|
+
from .ast_nodes import ExpressionStatement, IfStatement, Reassignment, VarDecl, VaripDecl
|
|
189
|
+
if isinstance(item, (Assignment, Reassignment, VarDecl, VaripDecl)):
|
|
190
|
+
return item.value
|
|
191
|
+
if isinstance(item, ExpressionStatement):
|
|
192
|
+
return item.expr
|
|
193
|
+
if isinstance(item, IfStatement):
|
|
194
|
+
return item.condition
|
|
195
|
+
return []
|