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,422 @@
1
+ """Type annotations the way PyneComp writes them.
2
+
3
+ * A name is a series (``Series``, ``PersistentSeries``) only when its scope
4
+ reads it with history (``x[1]``); a ``var`` otherwise is ``Persistent``.
5
+ * A declaration (the first assignment of a name in a scope; later ones are
6
+ Pine ``:=``) is annotated when its type is known without inference: the Pine
7
+ source types it (``float x = ...``) or its value says it — a literal, a hex
8
+ colour, ``array.new_float()``, ``array.from_items('a', 'b')``, ``line.all``.
9
+ * A series or persistent annotation keeps its element type (``Series[float]``)
10
+ only then; otherwise it is bare (``Series``).
11
+
12
+ Measured on ``assets/pyne``: 2626 ``str``, 1254 ``int``, 1048 ``float``, 1146
13
+ ``bool``, 941 ``Color`` declarations; ``Series`` 2108 bare against 859 typed.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import ast
18
+ import re
19
+
20
+ from .base import PassContext, function_scopes, own_nodes
21
+
22
+ _LITERAL_TYPES = {bool: 'bool', int: 'int', float: 'float', str: 'str'}
23
+ _ARRAY_ELEMENTS = {
24
+ 'new_float': 'float', 'new_int': 'int', 'new_bool': 'bool', 'new_string': 'str', 'new_color': 'Color',
25
+ 'new_line': 'Line', 'new_label': 'Label', 'new_box': 'Box', 'new_table': 'Table',
26
+ 'new_linefill': 'LineFill', 'new_polyline': 'Polyline',
27
+ }
28
+ _ALL_COLLECTIONS = {'line': 'Line', 'label': 'Label', 'box': 'Box', 'table': 'Table',
29
+ 'linefill': 'LineFill', 'polyline': 'Polyline'}
30
+ _SERIES_KINDS = {'Series': None, 'PersistentSeries': 'Persistent', 'IBPersistentSeries': 'IBPersistent'}
31
+ _PERSISTENT_KINDS = ('Series', 'PersistentSeries', 'IBPersistentSeries', 'Persistent', 'IBPersistent')
32
+
33
+
34
+ def _dotted(node: ast.AST) -> str:
35
+ if isinstance(node, ast.Attribute):
36
+ return f'{_dotted(node.value)}.{node.attr}'
37
+ return node.id if isinstance(node, ast.Name) else ''
38
+
39
+
40
+ def literal_type(value: ast.expr | None) -> str | None:
41
+ if isinstance(value, ast.Constant):
42
+ return _LITERAL_TYPES.get(type(value.value))
43
+ if (isinstance(value, ast.UnaryOp) and isinstance(value.op, ast.USub)
44
+ and isinstance(value.operand, ast.Constant) and type(value.operand.value) is int):
45
+ return 'int'
46
+ return None
47
+
48
+
49
+ def _typed_na_argument(call: ast.Call) -> str | None:
50
+ """T of the `NA(T)` value of `array.new(n, NA(T))` / `matrix.new(..., initial_value=NA(T))`."""
51
+ position = 2 if _dotted(call.func) == 'matrix.new' else 1 # `matrix.new(r, c, NA(T))`
52
+ values = list(call.args[position:position + 1]) + [keyword.value for keyword in call.keywords
53
+ if keyword.arg == 'initial_value']
54
+ for value in values:
55
+ if (isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id == 'NA'
56
+ and len(value.args) == 1 and isinstance(value.args[0], ast.Name)):
57
+ return value.args[0].id
58
+ return None
59
+
60
+
61
+ def inferable_type(value: ast.expr | None, classes: frozenset = frozenset(),
62
+ methods: frozenset = frozenset()) -> str | None:
63
+ """The type PyneComp reads off a value without inference, or None. ``classes``:
64
+ the module's UDT classes, whose constructor call names its type; ``methods``: the user
65
+ methods, whose call on a constructor (`T.new().m()`) PyneComp types as `T` (probe mc5)."""
66
+ found = literal_type(value)
67
+ if found:
68
+ return found
69
+ if isinstance(value, ast.Attribute) and value.attr == 'all' and isinstance(value.value, ast.Name):
70
+ element = _ALL_COLLECTIONS.get(value.value.id)
71
+ return f'list[{element}]' if element else None
72
+ if not isinstance(value, ast.Call):
73
+ return None
74
+ function = _dotted(value.func)
75
+ if function in classes:
76
+ return function
77
+ if function.endswith('.new') and function.count('.') == 2 and function.split('.')[1][:1].isupper():
78
+ return function[:-len('.new')] # a library's type, `dr.AuxData.new(...)` (biased_price_target)
79
+ receiver = None
80
+ if function == 'method_call' and len(value.args) >= 2:
81
+ receiver = value.args[1]
82
+ elif function in methods and value.args:
83
+ receiver = value.args[0]
84
+ if isinstance(receiver, ast.Call):
85
+ return inferable_type(receiver, classes, methods) if _dotted(receiver.func) in classes | methods \
86
+ or _dotted(receiver.func) == 'method_call' else None
87
+ if function == 'color.new' and len(value.args) == 1 and not value.keywords \
88
+ and isinstance(value.args[0], ast.Constant) and isinstance(value.args[0].value, str):
89
+ return 'Color' # a hex colour literal
90
+ if function == 'matrix.new' and len(value.args) >= 3 and literal_type(value.args[2]):
91
+ return f'Matrix[{literal_type(value.args[2])}]' # the element type of the initial value
92
+ typed_na = _typed_na_argument(value)
93
+ if function in ('matrix.new', 'array.new') and typed_na:
94
+ return f"{'Matrix' if function == 'matrix.new' else 'list'}[{typed_na}]" # `NA(T)`: of T
95
+ if function.startswith('array.'):
96
+ element = _ARRAY_ELEMENTS.get(function[len('array.'):])
97
+ if element:
98
+ return f'list[{element}]'
99
+ if function == 'array.from_items' and value.args:
100
+ types = {literal_type(argument) for argument in value.args}
101
+ if len(types) == 1 and None not in types:
102
+ return f'list[{types.pop()}]'
103
+ return None
104
+
105
+
106
+ class DeclaredTypes:
107
+ """The Python types of the Pine source's typed declarations (``float x = ...``),
108
+ looked up by name and, when the statement's Pine line is known, by that line."""
109
+
110
+ def __init__(self, script) -> None:
111
+ from ....ast_nodes import Assignment, VarDecl, VaripDecl
112
+ from ....ast_walk import walk
113
+ from ....transformer import convert_pine_generic_to_python
114
+ self.by_line: dict = {}
115
+ self.by_name: dict = {}
116
+ self.declared_lines: dict = {}
117
+ for node in walk([getattr(script, 'declarations', []), getattr(script, 'body', [])]):
118
+ if isinstance(node, (Assignment, VarDecl, VaripDecl)):
119
+ name = node.target if isinstance(node, Assignment) else node.name
120
+ if not isinstance(name, str):
121
+ continue
122
+ line = getattr(node, 'line', 0)
123
+ self.declared_lines.setdefault(name, set()).add(line)
124
+ type_hint = getattr(node, 'type_hint', None) or self._generic_constructor_type(node.value)
125
+ if type_hint:
126
+ python_type = convert_pine_generic_to_python(type_hint)
127
+ self.by_line[(name, line)] = python_type
128
+ self.by_name.setdefault(name, python_type)
129
+
130
+ @staticmethod
131
+ def _generic_constructor_type(value) -> str | None:
132
+ """`map.new<string, bool>()` names its type, `map<string, bool>` (probe nb4_02); `T.copy(x)` names `T`
133
+ (Trendoscope/Zigzag `lPivot: Pivot = udt_copy(...)`)."""
134
+ from ....ast_nodes import FunctionCall
135
+ if isinstance(value, FunctionCall) and isinstance(value.func, str) and value.func.startswith('map.new<'):
136
+ return 'map' + value.func[len('map.new'):]
137
+ if isinstance(value, FunctionCall) and isinstance(value.func, str) and value.func.endswith('.copy') \
138
+ and value.func.count('.') == 1 and value.func[:1].isupper() and len(value.args) == 1:
139
+ return value.func[:-len('.copy')]
140
+ return None
141
+
142
+ #: the suffixes renaming gives a Pine name: `x__000000c0__`, `x__ren__`, `x__global__`
143
+ _RENAMED = re.compile(r'__[0-9a-f]{8}__$|__ren__$|__global__$')
144
+
145
+ def get(self, name: str, line: int = 0):
146
+ name = self._RENAMED.sub('', name)
147
+ if line and line in self.declared_lines.get(name, ()):
148
+ return self.by_line.get((name, line))
149
+ return self.by_name.get(name)
150
+
151
+ def __contains__(self, name: str) -> bool:
152
+ return self._RENAMED.sub('', name) in self.by_name
153
+
154
+
155
+ def pine_declared_types(script) -> DeclaredTypes:
156
+ return DeclaredTypes(script)
157
+
158
+
159
+ class SeriesAnnotationPass:
160
+ """A name is a series only where its scope reads it with history: otherwise
161
+ ``PersistentSeries[T]`` is ``Persistent[T]`` and a ``Series`` annotation goes."""
162
+
163
+ @staticmethod
164
+ def _history_reads(scope: ast.AST) -> set[str]:
165
+ """Names `scope` reads with history, its nested functions included where they do
166
+ not bind the name themselves (a parameter `min` is not main's `min`)."""
167
+ reads = {node.value.id for node in own_nodes(scope)
168
+ if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name)}
169
+ for node in own_nodes(scope):
170
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
171
+ arguments = node.args
172
+ bound = {arg.arg for arg in arguments.posonlyargs + arguments.args + arguments.kwonlyargs}
173
+ bound |= {child.id for child in own_nodes(node)
174
+ if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store)}
175
+ reads |= SeriesAnnotationPass._history_reads(node) - bound
176
+ return reads
177
+
178
+ def apply(self, tree: ast.Module, context: PassContext) -> None:
179
+ for scope in function_scopes(tree):
180
+ read_with_history = self._history_reads(scope)
181
+ blocks = [node for node in own_nodes(scope) if hasattr(node, 'body')
182
+ and not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))]
183
+ for owner in [scope] + blocks:
184
+ for field in ('body', 'orelse', 'finalbody'):
185
+ block = getattr(owner, field, None)
186
+ if isinstance(block, list):
187
+ block[:] = [self._demoted(statement, read_with_history) for statement in block]
188
+
189
+ @staticmethod
190
+ def _demoted(statement: ast.stmt, read_with_history: set) -> ast.stmt:
191
+ if not (isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name)
192
+ and statement.target.id not in read_with_history):
193
+ return statement
194
+ annotation = statement.annotation
195
+ head = annotation.value if isinstance(annotation, ast.Subscript) else annotation
196
+ if not isinstance(head, ast.Name) or head.id not in _SERIES_KINDS:
197
+ return statement
198
+ replacement = _SERIES_KINDS[head.id]
199
+ if replacement is None: # a plain series: no annotation
200
+ if statement.value is None:
201
+ return statement
202
+ return ast.copy_location(ast.Assign(targets=[statement.target], value=statement.value), statement)
203
+ if isinstance(annotation, ast.Subscript):
204
+ annotation.value = ast.Name(id=replacement, ctx=ast.Load())
205
+ else:
206
+ statement.annotation = ast.Name(id=replacement, ctx=ast.Load())
207
+ return statement
208
+
209
+
210
+ def _unquoted(annotation) -> str:
211
+ """An annotation's text, a string annotation (`'list[Pivot]'`) unquoted."""
212
+ if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
213
+ return annotation.value
214
+ return ast.unparse(annotation)
215
+
216
+
217
+ def _element_of_kind(annotation: str | None) -> str | None:
218
+ """`Persistent[Pivot]`, `Series[Pivot]` -> `Pivot`."""
219
+ if annotation is None:
220
+ return None
221
+ match = re.fullmatch(r'(?:Persistent|PersistentSeries|Series|IBPersistent|IBPersistentSeries)\[(.*)\]', annotation)
222
+ return match.group(1) if match else annotation
223
+
224
+
225
+ class DeclarationAnnotationPass:
226
+ """Declarations carry a type PyneComp knows; series and persistent annotations
227
+ keep their element type only then (see the module docstring)."""
228
+
229
+ def apply(self, tree: ast.Module, context: PassContext) -> None:
230
+ typed = pine_declared_types(context.unit.pine_script)
231
+ self._origins = context.origins
232
+ self._classes = frozenset(node.name for node in tree.body if isinstance(node, ast.ClassDef))
233
+ self._methods = frozenset(node.name for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)
234
+ and any(isinstance(d, ast.Name) and d.id == 'method' for d in node.decorator_list))
235
+ # each UDT's fields and their annotations: a field read is typed by its field (probe nodes1)
236
+ self._fields = {node.name: {field.target.id: _unquoted(field.annotation) for field in node.body
237
+ if isinstance(field, ast.AnnAssign) and isinstance(field.target, ast.Name)}
238
+ for node in tree.body if isinstance(node, ast.ClassDef)}
239
+ for scope in function_scopes(tree):
240
+ declared: set[str] = set()
241
+ types: dict[str, str] = {}
242
+ if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef)):
243
+ arguments = scope.args
244
+ for arg in arguments.posonlyargs + arguments.args + arguments.kwonlyargs:
245
+ declared.add(arg.arg)
246
+ if arg.annotation is not None:
247
+ types[arg.arg] = _unquoted(arg.annotation)
248
+ self._annotate(scope.body, declared, typed, types)
249
+
250
+ _classes: frozenset = frozenset()
251
+ _methods: frozenset = frozenset()
252
+ _fields: dict = {}
253
+ _origins: dict = {}
254
+
255
+ def _known(self, name: str, value, typed, statement=None) -> str | None:
256
+ line = self._origins.get(getattr(statement, 'lineno', 0), 0) if statement is not None else 0
257
+ return typed.get(name, line) or inferable_type(value, self._classes, self._methods)
258
+
259
+ def _bare_unless_known(self, statement: ast.AnnAssign, typed: dict[str, str],
260
+ types: dict[str, str] | None = None) -> None:
261
+ annotation = statement.annotation
262
+ if isinstance(annotation, ast.Name) and annotation.id in _PERSISTENT_KINDS:
263
+ # a bare kind takes the type of a constructor's method chain (`var w = T.new().init()`), of a
264
+ # library type's constructor (`var s = lib.T.new()`) (chrono_utils, biased_price_target) or of the
265
+ # field it reads (`cl: Series[float] = c.c`, Trendoscope/oota)
266
+ element = self._constructed_type(statement.value) or self._field_type(statement.value, typed, types or {})
267
+ if element:
268
+ statement.annotation = ast.Subscript(value=annotation, slice=ast.parse(element, mode='eval').body,
269
+ ctx=ast.Load())
270
+ return
271
+ if not (isinstance(annotation, ast.Subscript) and isinstance(annotation.value, ast.Name)
272
+ and annotation.value.id in _PERSISTENT_KINDS):
273
+ return
274
+ if not (self._known(statement.target.id, statement.value, typed, statement)
275
+ or self._field_type(statement.value, typed, types or {})):
276
+ statement.annotation = ast.Name(id=annotation.value.id, ctx=ast.Load())
277
+ elif literal_type(statement.value) and statement.target.id not in typed:
278
+ # a literal names the element type itself: `x: Series[float] = 0.0`
279
+ annotation.slice = ast.Name(id=literal_type(statement.value), ctx=ast.Load())
280
+
281
+ def _constructed_type(self, value) -> str | None:
282
+ if isinstance(value, ast.Call):
283
+ function = _dotted(value.func)
284
+ if function.endswith('.new') and function.count('.') == 2 and function.split('.')[1][:1].isupper():
285
+ return function[:-len('.new')] # `dr.DistSettings.new(...)`
286
+ if function in self._methods or function == 'method_call':
287
+ return inferable_type(value, self._classes, self._methods)
288
+ return None
289
+
290
+ def _field_type(self, value, typed, types: dict[str, str]) -> str | None:
291
+ """The declared type of the UDT field `value` reads (`p.x`, `a.b.c`), when its receiver's type is known."""
292
+ if not isinstance(value, ast.Attribute):
293
+ return None
294
+ receiver = value.value
295
+ if isinstance(receiver, ast.Name):
296
+ owner = types.get(receiver.id) # only a type this scope declares (Zigzag `llastPivot` is untyped)
297
+ else:
298
+ owner = self._field_type(receiver, typed, types)
299
+ owner = _element_of_kind(owner)
300
+ return self._fields.get(owner, {}).get(value.attr) if owner else None
301
+
302
+ def _annotate(self, body: list[ast.stmt], declared: set[str], typed: dict[str, str],
303
+ types: dict[str, str] | None = None) -> None:
304
+ types = {} if types is None else types
305
+ for index, statement in enumerate(body):
306
+ if isinstance(statement, ast.Assign) and len(statement.targets) == 1 \
307
+ and isinstance(statement.targets[0], ast.Name):
308
+ name = statement.targets[0].id
309
+ value = statement.value
310
+ if isinstance(value, ast.Attribute) and isinstance(value.value, ast.Name) \
311
+ and re.fullmatch(r'__loop_\d+__', value.value.id):
312
+ declared.add(name) # a `pine_loop` variable's value is never annotated (analytics_tables)
313
+ continue
314
+ annotation = self._known(name, statement.value, typed, statement)
315
+ if annotation is None and name not in declared:
316
+ annotation = self._field_type(statement.value, typed, types)
317
+ if name not in declared and annotation and not name.startswith('__'):
318
+ body[index] = ast.copy_location(ast.AnnAssign(
319
+ target=statement.targets[0], annotation=ast.parse(annotation, mode='eval').body,
320
+ value=statement.value, simple=1), statement)
321
+ types[name] = annotation
322
+ declared.add(name)
323
+ elif isinstance(statement, ast.Assign): # `a, b = f()` declares its names
324
+ declared.update(node.id for target in statement.targets for node in ast.walk(target)
325
+ if isinstance(node, ast.Name))
326
+ elif isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name):
327
+ self._bare_unless_known(statement, typed, types)
328
+ declared.add(statement.target.id)
329
+ types[statement.target.id] = _unquoted(statement.annotation)
330
+ elif isinstance(statement, (ast.For, ast.AsyncFor)):
331
+ declared.update(node.id for node in ast.walk(statement.target) if isinstance(node, ast.Name))
332
+ elif isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
333
+ declared.add(statement.name)
334
+ continue
335
+ for name in ('body', 'orelse', 'finalbody'):
336
+ nested = getattr(statement, name, None)
337
+ if isinstance(nested, list):
338
+ # a Pine block is a scope: what it declares is unknown to its siblings
339
+ self._annotate(nested, set(declared), typed, dict(types))
340
+
341
+
342
+ class ColorCastPass:
343
+ """A colour value is not wrapped in ``Color(...)``: ``GREEN: Color = color.new('#00FF00')``."""
344
+
345
+ def apply(self, tree: ast.Module, context: PassContext) -> None:
346
+ for node in ast.walk(tree):
347
+ for field, value in ast.iter_fields(node):
348
+ if (isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id == 'Color'
349
+ and len(value.args) == 1 and not value.keywords and isinstance(value.args[0], ast.Call)):
350
+ setattr(node, field, value.args[0])
351
+
352
+
353
+ class ClosureHistoryPass:
354
+ """A function that reads the history of a variable of its enclosing function
355
+ (``avg[1]`` with ``avg`` a main() variable) reads it from its own series,
356
+ ``__hist_avg__: Series = avg[0]``, bound first in its body (33 modules of the reference)."""
357
+
358
+ PREFIX, SUFFIX = '__hist_', '__'
359
+
360
+ def apply(self, tree: ast.Module, context: PassContext) -> None:
361
+ main = next((node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == 'main'), None)
362
+ if main is not None:
363
+ # the snapshot keeps the element type the variable is declared with in main():
364
+ # `x: PersistentSeries[float]` or `x: float` gives `__hist_x__: Series[float]`
365
+ self._series_types = {}
366
+ declared = [(arg.arg, arg.annotation) for arg in main.args.args]
367
+ declared += [(node.target.id, node.annotation) for node in own_nodes(main)
368
+ if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name)]
369
+ for name, annotation in declared:
370
+ element = self._element_type(annotation)
371
+ if element is not None and name not in self._series_types:
372
+ self._series_types[name] = ast.Subscript(value=ast.Name(id='Series', ctx=ast.Load()),
373
+ slice=element, ctx=ast.Load())
374
+ self._visit(main, set())
375
+
376
+ _series_types: dict = {}
377
+
378
+ @staticmethod
379
+ def _element_type(annotation):
380
+ if annotation is None:
381
+ return None
382
+ if isinstance(annotation, ast.Subscript) and isinstance(annotation.value, ast.Name) \
383
+ and annotation.value.id in _PERSISTENT_KINDS:
384
+ return annotation.slice
385
+ if isinstance(annotation, ast.Name) and annotation.id not in _PERSISTENT_KINDS:
386
+ return annotation
387
+ return None
388
+
389
+ @staticmethod
390
+ def _own_bindings(function: ast.FunctionDef) -> set[str]:
391
+ arguments = function.args
392
+ bound = {arg.arg for arg in arguments.posonlyargs + arguments.args + arguments.kwonlyargs}
393
+ for node in own_nodes(function):
394
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
395
+ bound.add(node.id)
396
+ elif isinstance(node, (ast.FunctionDef, ast.ClassDef)):
397
+ bound.add(node.name)
398
+ return bound
399
+
400
+ def _visit(self, function: ast.FunctionDef, enclosing: set[str]) -> None:
401
+ own = self._own_bindings(function)
402
+ if enclosing:
403
+ snapshots: list[str] = []
404
+ for node in ast.walk(function):
405
+ if (isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name)
406
+ and node.value.id in enclosing and node.value.id not in own
407
+ and node.value.id not in snapshots):
408
+ snapshots.append(node.value.id)
409
+ if snapshots:
410
+ for node in ast.walk(function):
411
+ if (isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name)
412
+ and node.value.id in snapshots):
413
+ node.value = ast.Name(id=f'{self.PREFIX}{node.value.id}{self.SUFFIX}', ctx=ast.Load())
414
+ first = function.body[0] if function.body else function
415
+ function.body[:0] = [ast.copy_location(ast.AnnAssign(
416
+ target=ast.Name(id=f'{self.PREFIX}{name}{self.SUFFIX}', ctx=ast.Store()),
417
+ annotation=self._series_types.get(name) or ast.Name(id='Series', ctx=ast.Load()),
418
+ value=ast.Subscript(value=ast.Name(id=name, ctx=ast.Load()), slice=ast.Constant(0),
419
+ ctx=ast.Load()), simple=1), first) for name in snapshots]
420
+ for node in own_nodes(function):
421
+ if isinstance(node, ast.FunctionDef):
422
+ self._visit(node, (enclosing | own) - self._own_bindings(node))
@@ -0,0 +1,69 @@
1
+ """Rewrites of the generated Python module towards PyneComp's output.
2
+
3
+ pine2pyne and PyneComp agree on what a script computes but often spell it
4
+ differently (``PersistentSeries`` vs ``Persistent``, ``'#00ced1'`` vs
5
+ ``color.new('#00CED1')``). Each difference is one :class:`ModulePass`: a
6
+ small rewrite of the Python AST, applied in order by the PyneComp emitter.
7
+ A new difference is a new pass; existing passes stay untouched.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import ast
12
+ from dataclasses import dataclass, field
13
+ from typing import Iterator, Protocol
14
+
15
+ from ....core.contracts import CompilationUnit
16
+
17
+
18
+ @dataclass
19
+ class PassContext:
20
+ unit: CompilationUnit
21
+ #: exact text to print for a node (``id(node)``), e.g. a title's source quotes
22
+ spellings: dict[int, str] = field(default_factory=dict)
23
+ #: Python line -> the Pine line of the statement generated there
24
+ origins: dict[int, int] = field(default_factory=dict)
25
+
26
+ @property
27
+ def pine_version(self) -> int:
28
+ return self.unit.pine_version or 6
29
+
30
+
31
+ class ModulePass(Protocol):
32
+ def apply(self, tree: ast.Module, context: PassContext) -> None: ...
33
+
34
+
35
+ def main_function(tree: ast.Module) -> ast.FunctionDef | None:
36
+ for node in tree.body:
37
+ if isinstance(node, ast.FunctionDef) and node.name == 'main':
38
+ return node
39
+ return None
40
+
41
+
42
+ def function_scopes(tree: ast.Module) -> Iterator[ast.AST]:
43
+ """The module and every function in it."""
44
+ yield tree
45
+ for node in ast.walk(tree):
46
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
47
+ yield node
48
+
49
+
50
+ def own_nodes(scope: ast.AST) -> Iterator[ast.AST]:
51
+ """Nodes of ``scope`` that are not inside a nested function or class."""
52
+ stack = list(ast.iter_child_nodes(scope))
53
+ while stack:
54
+ node = stack.pop()
55
+ yield node
56
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
57
+ stack.extend(ast.iter_child_nodes(node))
58
+
59
+
60
+ def statements_in_order(body: list[ast.stmt]) -> Iterator[ast.stmt]:
61
+ """Statements of a block and its nested blocks (not nested functions), in source order."""
62
+ for statement in body:
63
+ yield statement
64
+ if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
65
+ continue
66
+ for name in ('body', 'orelse', 'finalbody'):
67
+ nested = getattr(statement, name, None)
68
+ if isinstance(nested, list):
69
+ yield from statements_in_order(nested)
@@ -0,0 +1,169 @@
1
+ """Built-in calls spelled the way PyneComp spells them."""
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+
6
+ from .base import PassContext
7
+
8
+ #: ``ta`` members that are series values, not calls (``ta.tr(true)`` is the call form)
9
+ TA_SERIES = frozenset({'tr', 'vwap', 'obv', 'accdist', 'iii', 'nvi', 'pvi', 'pvt', 'wad', 'wvad'})
10
+ #: Pine v4 hline and plot styles were integers
11
+ _HLINE_STYLES = {0: 'style_solid', 1: 'style_dotted', 2: 'style_dashed'}
12
+ _PLOT_STYLES = {0: 'style_line', 1: 'style_stepline', 2: 'style_histogram', 3: 'style_cross', 4: 'style_area',
13
+ 5: 'style_columns', 6: 'style_circles'}
14
+
15
+
16
+ def _dotted(node: ast.AST) -> str:
17
+ if isinstance(node, ast.Attribute):
18
+ return f'{_dotted(node.value)}.{node.attr}'
19
+ return node.id if isinstance(node, ast.Name) else ''
20
+
21
+
22
+ def _attribute(base: str, member: str) -> ast.Attribute:
23
+ return ast.Attribute(value=ast.Name(id=base, ctx=ast.Load()), attr=member, ctx=ast.Load())
24
+
25
+
26
+ def _bound_names(tree: ast.Module) -> set[str]:
27
+ bound = {node.id for node in ast.walk(tree) if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store)}
28
+ bound |= {node.arg for node in ast.walk(tree) if isinstance(node, ast.arg)}
29
+ bound |= {node.name for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.ClassDef))}
30
+ return bound
31
+
32
+
33
+ class _Replacer(ast.NodeTransformer):
34
+ def __init__(self, replace):
35
+ self._replace = replace
36
+
37
+ def generic_visit(self, node):
38
+ node = super().generic_visit(node)
39
+ replaced = self._replace(node)
40
+ return node if replaced is None else ast.copy_location(replaced, node)
41
+
42
+
43
+ class TaSeriesPass:
44
+ """``ta.tr``, ``ta.vwap``, ``ta.obv``, ... are values: no call without arguments;
45
+ a Pine v4 bare ``tr``/``vwap``/... is the ``ta.`` value."""
46
+
47
+ def apply(self, tree: ast.Module, context: PassContext) -> None:
48
+ bound = _bound_names(tree)
49
+ legacy = context.pine_version <= 4
50
+
51
+ def replace(node):
52
+ if (isinstance(node, ast.Call) and not node.args and not node.keywords
53
+ and isinstance(node.func, ast.Attribute) and node.func.attr in TA_SERIES
54
+ and _dotted(node.func.value) == 'ta'):
55
+ return node.func
56
+ if (legacy and isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load)
57
+ and node.id in TA_SERIES and node.id not in bound):
58
+ return _attribute('ta', node.id)
59
+ return None
60
+ _Replacer(replace).visit(tree)
61
+
62
+
63
+ class LegacyStrategyDirectionPass:
64
+ """Pine v4 ``strategy.entry(id, true)`` / ``long=true``: the direction is
65
+ ``strategy.long`` / ``strategy.short`` (``direction=`` for the keyword)."""
66
+
67
+ FUNCTIONS = frozenset({'strategy.entry', 'strategy.order'})
68
+
69
+ def apply(self, tree: ast.Module, context: PassContext) -> None:
70
+ if context.pine_version > 4:
71
+ return
72
+ for node in ast.walk(tree):
73
+ if isinstance(node, ast.Call) and _dotted(node.func) in self.FUNCTIONS:
74
+ if len(node.args) >= 2:
75
+ node.args[1] = self._direction(node.args[1])
76
+ for keyword in node.keywords:
77
+ if keyword.arg == 'long':
78
+ keyword.arg, keyword.value = 'direction', self._direction(keyword.value)
79
+
80
+ @staticmethod
81
+ def _direction(value: ast.expr) -> ast.expr:
82
+ if isinstance(value, ast.Constant) and value.value in (True, False, 1, 0):
83
+ return _attribute('strategy', 'long' if value.value else 'short')
84
+ return value
85
+
86
+
87
+ class LegacyLineWidthPass:
88
+ """Pine v4/v5: a ``linewidth`` that is not a constant of at least 1 is ``math.max(1, width)``."""
89
+
90
+ def apply(self, tree: ast.Module, context: PassContext) -> None:
91
+ if context.pine_version >= 6:
92
+ return
93
+ for node in ast.walk(tree):
94
+ at_least_one = (isinstance(getattr(node, 'value', None), ast.Constant)
95
+ and isinstance(node.value.value, (int, float)) and node.value.value >= 1)
96
+ if isinstance(node, ast.keyword) and node.arg == 'linewidth' \
97
+ and not at_least_one and not self._is_max(node.value):
98
+ node.value = ast.Call(func=_attribute('math', 'max'), args=[ast.Constant(1), node.value], keywords=[])
99
+
100
+ @staticmethod
101
+ def _is_max(value: ast.expr) -> bool:
102
+ return isinstance(value, ast.Call) and _dotted(value.func) == 'math.max'
103
+
104
+
105
+ class CurrentValuePass:
106
+ """``x[0]`` is ``x``."""
107
+
108
+ def apply(self, tree: ast.Module, context: PassContext) -> None:
109
+ def replace(node):
110
+ if (isinstance(node, ast.Subscript) and isinstance(node.ctx, ast.Load)
111
+ and isinstance(node.slice, ast.Constant) and node.slice.value == 0
112
+ and isinstance(node.value, (ast.Name, ast.Attribute))
113
+ and not (isinstance(node.value, ast.Name) and node.value.id.startswith('__'))):
114
+ return node.value
115
+ return None
116
+ _Replacer(replace).visit(tree)
117
+
118
+
119
+ class LegacyHlineStylePass:
120
+ """Pine v4 ``hline(..., linestyle=2)`` / ``plot(..., style=5)``: the style constant,
121
+ ``hline.style_dashed`` / ``plot.style_columns``."""
122
+
123
+ _STYLES = {('hline', 'linestyle'): ('hline', _HLINE_STYLES), ('plot', 'style'): ('plot', _PLOT_STYLES)}
124
+
125
+ def apply(self, tree: ast.Module, context: PassContext) -> None:
126
+ if context.pine_version > 4:
127
+ return
128
+ for node in ast.walk(tree):
129
+ if not isinstance(node, ast.Call):
130
+ continue
131
+ for keyword in node.keywords:
132
+ namespace, styles = self._STYLES.get((_dotted(node.func), keyword.arg), (None, {}))
133
+ if (namespace and isinstance(keyword.value, ast.Constant)
134
+ and type(keyword.value.value) is int and keyword.value.value in styles):
135
+ keyword.value = _attribute(namespace, styles[keyword.value.value])
136
+
137
+
138
+ class InputOptionsPass:
139
+ """``options=`` is always a tuple: ``options=x`` (one name) is ``options=(x,)``."""
140
+
141
+ def apply(self, tree: ast.Module, context: PassContext) -> None:
142
+ for node in ast.walk(tree):
143
+ if isinstance(node, ast.Call) and _dotted(node.func).startswith('input'):
144
+ for keyword in node.keywords:
145
+ if keyword.arg == 'options' and not isinstance(keyword.value, (ast.Tuple, ast.List)):
146
+ keyword.value = ast.Tuple(elts=[keyword.value], ctx=ast.Load())
147
+
148
+
149
+ class ShadowedNamespacePass:
150
+ """A library imported under a built-in namespace's name (``import TradingView/ta/12``)
151
+ reads through ``ta__ren__ = shadowed_namespace(ta__ren____lib, ta)``: the library's
152
+ members first, the built-in's after. Every ``ta.`` of the script reads ``ta__ren__``."""
153
+
154
+ def apply(self, tree: ast.Module, context: PassContext) -> None:
155
+ shadowed = getattr(context.unit.transformed, 'shadowed_namespaces', None) or {}
156
+ if not shadowed:
157
+ return
158
+ for node in ast.walk(tree):
159
+ if (isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name)
160
+ and node.value.id in shadowed):
161
+ node.value = ast.Name(id=f'{node.value.id}__ren__', ctx=ast.Load())
162
+ position = max((index + 1 for index, node in enumerate(tree.body)
163
+ if isinstance(node, (ast.Import, ast.ImportFrom))), default=0)
164
+ tree.body[position:position] = [
165
+ ast.Assign(targets=[ast.Name(id=f'{alias}__ren__', ctx=ast.Store())],
166
+ value=ast.Call(func=ast.Name(id='shadowed_namespace', ctx=ast.Load()),
167
+ args=[ast.Name(id=module, ctx=ast.Load()), ast.Name(id=alias, ctx=ast.Load())],
168
+ keywords=[]), lineno=0, col_offset=0)
169
+ for alias, module in shadowed.items()]