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.
Files changed (75) hide show
  1. pine2pyne/__init__.py +84 -0
  2. pine2pyne/__main__.py +12 -0
  3. pine2pyne/ast_nodes.py +405 -0
  4. pine2pyne/ast_walk.py +93 -0
  5. pine2pyne/block_value_lowering.py +694 -0
  6. pine2pyne/bool_builtins.py +42 -0
  7. pine2pyne/bool_value_typing.py +580 -0
  8. pine2pyne/cli.py +247 -0
  9. pine2pyne/codegen.py +886 -0
  10. pine2pyne/compat/__init__.py +19 -0
  11. pine2pyne/compat/__main__.py +6 -0
  12. pine2pyne/compat/cli.py +114 -0
  13. pine2pyne/compat/comparison.py +105 -0
  14. pine2pyne/compat/corpus.py +73 -0
  15. pine2pyne/compat/report.py +95 -0
  16. pine2pyne/compat/runner.py +63 -0
  17. pine2pyne/condition_typing.py +260 -0
  18. pine2pyne/conditional_history_calls.py +96 -0
  19. pine2pyne/core/__init__.py +26 -0
  20. pine2pyne/core/contracts.py +125 -0
  21. pine2pyne/di.py +173 -0
  22. pine2pyne/emit/__init__.py +5 -0
  23. pine2pyne/emit/legacy.py +14 -0
  24. pine2pyne/emit/pynecomp/__init__.py +20 -0
  25. pine2pyne/emit/pynecomp/banner.py +30 -0
  26. pine2pyne/emit/pynecomp/blank_lines.py +69 -0
  27. pine2pyne/emit/pynecomp/emitter.py +61 -0
  28. pine2pyne/emit/pynecomp/imports.py +65 -0
  29. pine2pyne/emit/pynecomp/passes/__init__.py +84 -0
  30. pine2pyne/emit/pynecomp/passes/annotations.py +422 -0
  31. pine2pyne/emit/pynecomp/passes/base.py +69 -0
  32. pine2pyne/emit/pynecomp/passes/builtins.py +169 -0
  33. pine2pyne/emit/pynecomp/passes/hoisting.py +205 -0
  34. pine2pyne/emit/pynecomp/passes/imports.py +112 -0
  35. pine2pyne/emit/pynecomp/passes/legacy_colors.py +101 -0
  36. pine2pyne/emit/pynecomp/passes/library.py +205 -0
  37. pine2pyne/emit/pynecomp/passes/rewrites.py +215 -0
  38. pine2pyne/emit/pynecomp/printer.py +288 -0
  39. pine2pyne/emit/pynecomp/unparser.py +186 -0
  40. pine2pyne/emitted_names.py +152 -0
  41. pine2pyne/errors.py +69 -0
  42. pine2pyne/for_loop_end_bound.py +172 -0
  43. pine2pyne/grouped_history_lowering.py +310 -0
  44. pine2pyne/import_resolver.py +469 -0
  45. pine2pyne/input_hoisting.py +195 -0
  46. pine2pyne/legacy_function_names.py +74 -0
  47. pine2pyne/legacy_sessions.py +68 -0
  48. pine2pyne/lexer.py +630 -0
  49. pine2pyne/library_collision.py +434 -0
  50. pine2pyne/namespace_members.py +247 -0
  51. pine2pyne/node_ids.py +235 -0
  52. pine2pyne/once_lowering.py +75 -0
  53. pine2pyne/overload_resolution.py +475 -0
  54. pine2pyne/parse_repair.py +398 -0
  55. pine2pyne/parser.py +1873 -0
  56. pine2pyne/pine_builtins.py +394 -0
  57. pine2pyne/pine_semantics.py +604 -0
  58. pine2pyne/pipeline/__init__.py +18 -0
  59. pine2pyne/pipeline/service.py +70 -0
  60. pine2pyne/pipeline/stages.py +73 -0
  61. pine2pyne/scope_renamer.py +965 -0
  62. pine2pyne/strategy_when_lowering.py +101 -0
  63. pine2pyne/strict_boolean.py +101 -0
  64. pine2pyne/symbol_table.py +166 -0
  65. pine2pyne/tokens.py +143 -0
  66. pine2pyne/transformer.py +2510 -0
  67. pine2pyne/type_inference.py +422 -0
  68. pine2pyne/udt_field_defaults.py +149 -0
  69. pine2pyne/value_blocks.py +357 -0
  70. pine2pyne-0.2.4.dist-info/METADATA +99 -0
  71. pine2pyne-0.2.4.dist-info/RECORD +75 -0
  72. pine2pyne-0.2.4.dist-info/WHEEL +5 -0
  73. pine2pyne-0.2.4.dist-info/entry_points.txt +3 -0
  74. pine2pyne-0.2.4.dist-info/licenses/LICENSE +201 -0
  75. 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 []