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,260 @@
|
|
|
1
|
+
"""Which values PyneComp reads as numbers where Pine wants a condition.
|
|
2
|
+
|
|
3
|
+
A number in a condition is cast: ``if x`` is ``if cast_bool(x):``. PyneComp
|
|
4
|
+
decides it from what it knows of the value's type; probe scripts compiled by
|
|
5
|
+
PyneComp v6.0.80 (``tmp/probes/cb4..6.pine``) and the reference show what that is.
|
|
6
|
+
|
|
7
|
+
A *name* (or its history, or a literal) is typed from its declaration, at any
|
|
8
|
+
distance: a number literal, a built-in price or time series (``close``,
|
|
9
|
+
``bar_index``), arithmetic over numbers, ``time()``, ``nz()``, ``math.*``, the
|
|
10
|
+
``ta.*`` calls that do not give a bool, a number input, a cast. An arm that is na
|
|
11
|
+
does not count (``h = c ? close : na`` is a number), except in a typed declaration
|
|
12
|
+
read outside an ``if``/``while`` header: there ``float x = c ? close : na`` is not,
|
|
13
|
+
while the header reads the declared type (and ``float x = na`` is a number
|
|
14
|
+
everywhere). A name holding a user function's result is a number except in an
|
|
15
|
+
``if``/``while`` header. Such a name is cast in every condition but under ``not``
|
|
16
|
+
(where only a pivot is).
|
|
17
|
+
|
|
18
|
+
An *expression* written in the condition is cast only when it is ``ta.change(x)``
|
|
19
|
+
of a number, ``time(...)``/``time_close(...)`` or a pivot, or, as the condition of
|
|
20
|
+
a ternary that does not choose colours, arithmetic over numbers. ``nz()``,
|
|
21
|
+
``math.*``, ``array.size()`` and user function calls written there are not.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Dict, Optional
|
|
26
|
+
|
|
27
|
+
from .ast_nodes import (
|
|
28
|
+
Assignment, BinaryOp, FuncDecl, FunctionCall, Identifier, IfExpression, IndexAccess, InputDecl, Literal,
|
|
29
|
+
NaLiteral, Script, SwitchExpression, TernaryOp, UnaryOp, VarDecl, VaripDecl,
|
|
30
|
+
)
|
|
31
|
+
from .ast_walk import walk
|
|
32
|
+
from .bool_builtins import BOOL_BUILTINS
|
|
33
|
+
|
|
34
|
+
#: the built-in series that are numbers
|
|
35
|
+
NUMERIC_SERIES = frozenset({
|
|
36
|
+
'open', 'high', 'low', 'close', 'volume', 'hl2', 'hlc3', 'ohlc4', 'hlcc4', 'bar_index', 'last_bar_index',
|
|
37
|
+
'time', 'time_close', 'timenow', 'time_tradingday', 'last_bar_time', 'dayofweek', 'dayofmonth', 'hour',
|
|
38
|
+
'minute', 'second', 'month', 'year', 'weekofyear', 'ta.tr', 'ta.vwap', 'ta.obv', 'ta.accdist', 'ta.iii',
|
|
39
|
+
'ta.nvi', 'ta.pvi', 'ta.pvt', 'ta.wad', 'ta.wvad'})
|
|
40
|
+
#: calls whose result is a number
|
|
41
|
+
NUMERIC_CALLS = frozenset({'time', 'time_close', 'timestamp', 'int', 'float', 'str.tonumber', 'input.int', 'abs', 'max', 'min',
|
|
42
|
+
'round', 'pow',
|
|
43
|
+
'input.float', 'input.source', 'input.price', 'input.time'})
|
|
44
|
+
#: calls that give their first argument's type
|
|
45
|
+
PASS_THROUGH_CALLS = frozenset({'nz', 'fixnan'})
|
|
46
|
+
PIVOTS = frozenset({'ta.pivothigh', 'ta.pivotlow'})
|
|
47
|
+
#: calls PyneComp casts where they are written as a condition
|
|
48
|
+
DIRECT_CASTS = frozenset({'time', 'time_close'}) | PIVOTS
|
|
49
|
+
ARITHMETIC = frozenset({'+', '-', '*', '/', '%'})
|
|
50
|
+
COMPARISONS = frozenset({'==', '!=', '<', '>', '<=', '>=', 'and', 'or'})
|
|
51
|
+
_DEPTH = 32
|
|
52
|
+
#: a name declared without a type PyneComp reads: a parameter or a tuple element
|
|
53
|
+
_UNTYPED = object()
|
|
54
|
+
#: a parameter declared without a type: beside it, an `and`/`or` operand is not cast (probes cb16, cb17)
|
|
55
|
+
_UNTYPED_PARAMETER = object()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _scope_key(function: FuncDecl) -> tuple:
|
|
59
|
+
"""A function's scope, the same for the copies later lowerings make of its declaration."""
|
|
60
|
+
return function.name, function.line, function.column, len(function.params)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_arithmetic(expression) -> bool:
|
|
64
|
+
return (isinstance(expression, UnaryOp) and expression.op in ('-', '+')) or (
|
|
65
|
+
isinstance(expression, BinaryOp) and expression.op in ARITHMETIC)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ConditionTyping:
|
|
69
|
+
"""The condition typing of one script (renamed, before the transformation)."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, script: Script) -> None:
|
|
72
|
+
self._functions: Dict[str, FuncDecl] = {}
|
|
73
|
+
for node in walk(script):
|
|
74
|
+
if isinstance(node, FuncDecl):
|
|
75
|
+
self._functions.setdefault(node.name, node)
|
|
76
|
+
#: the declarations of the script's top level, and of each function (by id) on its own
|
|
77
|
+
self._global_values: Dict[str, object] = {}
|
|
78
|
+
self._function_values: Dict[tuple, Dict[str, object]] = {}
|
|
79
|
+
self._function_parameters: Dict[tuple, set] = {}
|
|
80
|
+
self._parameters: set = set()
|
|
81
|
+
functions = [node for node in walk(script) if isinstance(node, FuncDecl)]
|
|
82
|
+
inside = set()
|
|
83
|
+
for function in functions:
|
|
84
|
+
# a typed parameter is its type (`b = vx * 2` with `float vx` is a number, probe cb13)
|
|
85
|
+
values: Dict[str, object] = {parameter.name: ((parameter.type_hint, NaLiteral())
|
|
86
|
+
if parameter.type_hint in ('int', 'float', 'bool')
|
|
87
|
+
else _UNTYPED_PARAMETER if not parameter.type_hint
|
|
88
|
+
else _UNTYPED)
|
|
89
|
+
for parameter in function.params}
|
|
90
|
+
for node in walk(function.body):
|
|
91
|
+
inside.add(id(node))
|
|
92
|
+
self._record(node, values)
|
|
93
|
+
self._function_values[_scope_key(function)] = values
|
|
94
|
+
self._function_parameters[_scope_key(function)] = {parameter.name for parameter in function.params}
|
|
95
|
+
for node in walk(script):
|
|
96
|
+
if id(node) not in inside and not isinstance(node, FuncDecl):
|
|
97
|
+
self._record(node, self._global_values)
|
|
98
|
+
self._values: Dict[str, object] = self._global_values
|
|
99
|
+
self._in_function = False
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _record(node, values: Dict[str, object]) -> None:
|
|
103
|
+
if isinstance(node, Assignment) and isinstance(node.target, str):
|
|
104
|
+
values.setdefault(node.target, (node.type_hint, node.value))
|
|
105
|
+
elif isinstance(node, Assignment) and hasattr(node.target, 'names'):
|
|
106
|
+
for name in node.target.names: # `[a, b] = f()`: untyped (probe-checked on mZigzag)
|
|
107
|
+
values.setdefault(name, _UNTYPED)
|
|
108
|
+
elif isinstance(node, (VarDecl, VaripDecl)):
|
|
109
|
+
values.setdefault(node.name, (node.type_hint, node.value))
|
|
110
|
+
elif isinstance(node, InputDecl):
|
|
111
|
+
values.setdefault(node.name, FunctionCall(func=node.func, args=node.args, kwargs=node.kwargs))
|
|
112
|
+
|
|
113
|
+
def enter(self, function: Optional[FuncDecl]) -> None:
|
|
114
|
+
"""Type names as `function` sees them (its own declarations first); None: the top level."""
|
|
115
|
+
local = self._function_values.get(_scope_key(function)) if function is not None else None
|
|
116
|
+
self._values = {**self._global_values, **local} if local is not None else self._global_values
|
|
117
|
+
self._parameters = self._function_parameters.get(_scope_key(function), set()) if function is not None else set()
|
|
118
|
+
self._in_function = local is not None
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------------------------ questions
|
|
121
|
+
|
|
122
|
+
def is_number(self, expression, header: bool = False, colour_ternary: bool = False,
|
|
123
|
+
ternary: bool = False) -> bool:
|
|
124
|
+
"""Whether PyneComp casts `expression`, written as a condition: `header` an if/while
|
|
125
|
+
condition, `ternary` the condition of a ternary (`colour_ternary`: one choosing colours)."""
|
|
126
|
+
written = expression
|
|
127
|
+
while isinstance(written, IndexAccess):
|
|
128
|
+
written = written.object
|
|
129
|
+
if isinstance(written, Identifier) and written.name in self._parameters:
|
|
130
|
+
return False # a parameter as a condition is never cast, typed or not (jordanfray library)
|
|
131
|
+
if isinstance(written, (Identifier, Literal)):
|
|
132
|
+
return self._kind(expression, header, _DEPTH) == 'number'
|
|
133
|
+
if isinstance(written, FunctionCall) and isinstance(written.func, str):
|
|
134
|
+
if written.func in DIRECT_CASTS or (written.func == 'ta.change' and written.args):
|
|
135
|
+
return self._kind(expression, header, _DEPTH) == 'number'
|
|
136
|
+
return False
|
|
137
|
+
if isinstance(written, (BinaryOp, UnaryOp)) and ternary and not colour_ternary:
|
|
138
|
+
return self._kind(expression, header, _DEPTH) == 'number'
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def has_a_type(self, expression) -> bool:
|
|
142
|
+
"""False for a parameter, beside which PyneComp casts no `and`/`or` operand (probes cb16, cb17)."""
|
|
143
|
+
return not (isinstance(expression, Identifier) and expression.name in self._parameters)
|
|
144
|
+
|
|
145
|
+
def is_bool(self, expression) -> bool:
|
|
146
|
+
"""Whether `expression` is a bool where it is compared (`flag == 1`); a history read is not."""
|
|
147
|
+
if isinstance(expression, IndexAccess):
|
|
148
|
+
return False
|
|
149
|
+
return self._kind(expression, False, _DEPTH) == 'bool'
|
|
150
|
+
|
|
151
|
+
def is_pivot(self, expression) -> bool:
|
|
152
|
+
"""`ta.pivothigh(...)`/`ta.pivotlow(...)`, or a name declared as one."""
|
|
153
|
+
for _ in range(_DEPTH):
|
|
154
|
+
if isinstance(expression, Identifier) and expression.name in self._values:
|
|
155
|
+
expression, _ = self._declared_value(expression.name, False)
|
|
156
|
+
continue
|
|
157
|
+
break
|
|
158
|
+
return isinstance(expression, FunctionCall) and expression.func in PIVOTS
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------ typing
|
|
161
|
+
|
|
162
|
+
def _declared_value(self, name: str, header: bool):
|
|
163
|
+
"""The value `name` is typed from, and whether an arm that is na leaves it untyped."""
|
|
164
|
+
value = self._values.get(name)
|
|
165
|
+
if value is _UNTYPED or value is _UNTYPED_PARAMETER:
|
|
166
|
+
return None, False
|
|
167
|
+
if not isinstance(value, tuple):
|
|
168
|
+
return value, False
|
|
169
|
+
type_hint, value = value
|
|
170
|
+
if type_hint == 'bool': # a number given to a bool declaration is cast there (`bool b = ta.change(t)`)
|
|
171
|
+
return Literal(value=True, literal_type='bool'), False
|
|
172
|
+
if type_hint in ('int', 'float') and (header or isinstance(value, NaLiteral) or value is None):
|
|
173
|
+
# an if/while header reads the declared type, as does every context for `float x = na`
|
|
174
|
+
return Literal(value=0 if type_hint != 'bool' else True, literal_type=type_hint), False
|
|
175
|
+
# elsewhere a typed declaration is typed from its value, where a na arm leaves it untyped
|
|
176
|
+
return value, type_hint is not None
|
|
177
|
+
|
|
178
|
+
def _kind(self, value, header: bool, depth: int, strict_na: bool = False) -> Optional[str]:
|
|
179
|
+
if depth <= 0 or value is None:
|
|
180
|
+
return None
|
|
181
|
+
depth -= 1
|
|
182
|
+
if isinstance(value, Literal):
|
|
183
|
+
if value.literal_type in ('int', 'float'):
|
|
184
|
+
return 'number'
|
|
185
|
+
return 'bool' if value.literal_type == 'bool' else None
|
|
186
|
+
if isinstance(value, NaLiteral):
|
|
187
|
+
return None
|
|
188
|
+
if isinstance(value, Identifier):
|
|
189
|
+
if value.name in ('true', 'false'):
|
|
190
|
+
return 'bool'
|
|
191
|
+
if value.name in NUMERIC_SERIES:
|
|
192
|
+
return 'number'
|
|
193
|
+
if value.name in self._values:
|
|
194
|
+
declared, strict = self._declared_value(value.name, header)
|
|
195
|
+
return self._kind(declared, header, depth, strict)
|
|
196
|
+
return None
|
|
197
|
+
if isinstance(value, IndexAccess):
|
|
198
|
+
return self._kind(value.object, header, depth)
|
|
199
|
+
if isinstance(value, UnaryOp):
|
|
200
|
+
if value.op == 'not':
|
|
201
|
+
return 'bool'
|
|
202
|
+
return self._kind(value.operand, header, depth)
|
|
203
|
+
if isinstance(value, BinaryOp):
|
|
204
|
+
if value.op in COMPARISONS:
|
|
205
|
+
return 'bool'
|
|
206
|
+
if value.op in ARITHMETIC:
|
|
207
|
+
kinds = {self._kind(value.left, header, depth), self._kind(value.right, header, depth)}
|
|
208
|
+
return 'number' if kinds == {'number'} else None
|
|
209
|
+
return None
|
|
210
|
+
if isinstance(value, TernaryOp):
|
|
211
|
+
arms = [value.true_expr, value.false_expr]
|
|
212
|
+
if strict_na and any(isinstance(arm, NaLiteral) for arm in arms):
|
|
213
|
+
return None # `float x = c ? low : na` read outside an if/while header
|
|
214
|
+
kinds = {self._kind(arm, header, depth, strict_na) for arm in arms if not isinstance(arm, NaLiteral)}
|
|
215
|
+
return kinds.pop() if len(kinds) == 1 else None
|
|
216
|
+
if isinstance(value, (IfExpression, SwitchExpression)):
|
|
217
|
+
return None
|
|
218
|
+
if isinstance(value, FunctionCall):
|
|
219
|
+
return self._call_kind(value, header, depth)
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
def _call_kind(self, call: FunctionCall, header: bool, depth: int) -> Optional[str]:
|
|
223
|
+
function = call.func if isinstance(call.func, str) else None
|
|
224
|
+
if function is None:
|
|
225
|
+
return None
|
|
226
|
+
if function in BOOL_BUILTINS or function in ('input.bool', 'bool'):
|
|
227
|
+
return 'bool'
|
|
228
|
+
if function in NUMERIC_CALLS or function.startswith('math.'):
|
|
229
|
+
return 'number'
|
|
230
|
+
if function in PASS_THROUGH_CALLS:
|
|
231
|
+
return self._kind(call.args[0], header, depth) if call.args else None
|
|
232
|
+
if function == 'ta.change' and call.args and self._kind(call.args[0], header, depth) == 'bool':
|
|
233
|
+
return 'bool' # the change of a bool is a bool
|
|
234
|
+
if function.startswith('ta.'):
|
|
235
|
+
return 'number'
|
|
236
|
+
if function == 'input':
|
|
237
|
+
default = call.args[0] if call.args else call.kwargs.get('defval')
|
|
238
|
+
return self._kind(default, header, depth)
|
|
239
|
+
if function in self._functions:
|
|
240
|
+
if header:
|
|
241
|
+
return None # PyneComp does not type a user function's result in an if/while header
|
|
242
|
+
if self._in_function:
|
|
243
|
+
# inside a function PyneComp reads no function's result type: a call is a number when
|
|
244
|
+
# an argument is arithmetic (`f(x, -1.0)`, `f(x, 0 - 1)`), else untyped (probes cb10..cb12)
|
|
245
|
+
arguments = list(call.args) + list(call.kwargs.values())
|
|
246
|
+
return 'number' if any(_is_arithmetic(argument) for argument in arguments) else None
|
|
247
|
+
declaration = self._functions[function]
|
|
248
|
+
body = declaration.body
|
|
249
|
+
tail = body[-1] if isinstance(body, list) and body else body
|
|
250
|
+
tail = getattr(tail, 'expr', tail)
|
|
251
|
+
# the tail reads the callee's names: its typed parameters are their type (probe cb8, `v * 2`)
|
|
252
|
+
outer = self._values
|
|
253
|
+
self._values = {**self._global_values, **self._function_values.get(_scope_key(declaration), {}),
|
|
254
|
+
**{parameter.name: (parameter.type_hint, NaLiteral()) for parameter in declaration.params
|
|
255
|
+
if parameter.type_hint in ('int', 'float', 'bool')}}
|
|
256
|
+
try:
|
|
257
|
+
return self._kind(tail, header, depth)
|
|
258
|
+
finally:
|
|
259
|
+
self._values = outer
|
|
260
|
+
return None
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""A `ta.*` call that runs on only some bars (card #96).
|
|
2
|
+
|
|
3
|
+
Pine keeps a call site's series history by running the call on every bar. A `ta.*` call
|
|
4
|
+
reached only when a condition holds keeps a history that skips the other bars, so its value
|
|
5
|
+
differs from the same call made on every bar (card #60 measured this: the transpiled output
|
|
6
|
+
matches Pine's call-site semantics in every construct, and a hoisted call gives another value).
|
|
7
|
+
The Pine User Manual (Execution model) says to run such functions in the global scope for
|
|
8
|
+
consistent results. The transpiler never hoists: it keeps the call where the script has it and
|
|
9
|
+
reports the site, as TradingView does, so the author decides.
|
|
10
|
+
|
|
11
|
+
A call is conditional when it sits in:
|
|
12
|
+
- the body of an `if`, `else if` or `else`, or an `else if` condition (it is read only when the
|
|
13
|
+
earlier ones failed), as a statement or as a value;
|
|
14
|
+
- a branch of `?:`;
|
|
15
|
+
- a `switch` arm or its default, or a case after the first (the first case is always read);
|
|
16
|
+
- the right operand of `and` / `or` in Pine v6, which evaluates them lazily. Pine v5 and earlier
|
|
17
|
+
evaluate both operands (card #65), so the right operand there is not conditional.
|
|
18
|
+
|
|
19
|
+
A function body starts unconditional again: whether the function is called conditionally is not
|
|
20
|
+
decided here. A loop body is not conditional. `ta.vwap` and the other argument-free `ta.`
|
|
21
|
+
variables are not calls and are hoisted by the transpiler already.
|
|
22
|
+
"""
|
|
23
|
+
from typing import List
|
|
24
|
+
|
|
25
|
+
from .ast_nodes import (
|
|
26
|
+
BinaryOp, FuncDecl, FunctionCall, IfExpression, IfStatement, Script, SwitchExpression, SwitchStatement,
|
|
27
|
+
TernaryOp,
|
|
28
|
+
)
|
|
29
|
+
from .ast_walk import child_field_names, is_node
|
|
30
|
+
from .strict_boolean import BOOLEAN_OPERATORS, evaluates_and_or_strictly
|
|
31
|
+
|
|
32
|
+
HISTORY_CALL_PREFIX = 'ta.'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def conditional_history_calls(script: Script) -> List[FunctionCall]:
|
|
36
|
+
"""Every `ta.*` call of `script` that runs on only some bars, in source order."""
|
|
37
|
+
finder = _ConditionalCallFinder(evaluates_and_or_strictly(script))
|
|
38
|
+
finder.visit(script, False)
|
|
39
|
+
return sorted(finder.calls, key=lambda call: (call.line, call.column))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _ConditionalCallFinder:
|
|
43
|
+
def __init__(self, right_operand_always_runs: bool):
|
|
44
|
+
self.right_operand_always_runs = right_operand_always_runs
|
|
45
|
+
self.calls: List[FunctionCall] = []
|
|
46
|
+
|
|
47
|
+
def visit(self, value, conditional: bool) -> None:
|
|
48
|
+
if isinstance(value, (list, tuple)):
|
|
49
|
+
for item in value:
|
|
50
|
+
self.visit(item, conditional)
|
|
51
|
+
elif isinstance(value, dict):
|
|
52
|
+
for item in value.values():
|
|
53
|
+
self.visit(item, conditional)
|
|
54
|
+
elif is_node(value):
|
|
55
|
+
self.visit_node(value, conditional)
|
|
56
|
+
|
|
57
|
+
def visit_node(self, node, conditional: bool) -> None:
|
|
58
|
+
if isinstance(node, (IfStatement, IfExpression, TernaryOp)):
|
|
59
|
+
self.visit_branches(node, conditional)
|
|
60
|
+
elif isinstance(node, (SwitchStatement, SwitchExpression)):
|
|
61
|
+
self.visit_switch(node, conditional)
|
|
62
|
+
elif isinstance(node, BinaryOp) and node.op in BOOLEAN_OPERATORS:
|
|
63
|
+
self.visit(node.left, conditional)
|
|
64
|
+
self.visit(node.right, conditional or not self.right_operand_always_runs)
|
|
65
|
+
elif isinstance(node, FuncDecl):
|
|
66
|
+
self.visit(node.params, False)
|
|
67
|
+
self.visit(node.body, False)
|
|
68
|
+
else:
|
|
69
|
+
if conditional and isinstance(node, FunctionCall) and _is_history_call(node):
|
|
70
|
+
self.calls.append(node)
|
|
71
|
+
for name in child_field_names(type(node)):
|
|
72
|
+
self.visit(getattr(node, name), conditional)
|
|
73
|
+
|
|
74
|
+
def visit_branches(self, node, conditional: bool) -> None:
|
|
75
|
+
"""The condition always runs; each branch and every `else if` condition only after it."""
|
|
76
|
+
self.visit(node.condition, conditional)
|
|
77
|
+
if isinstance(node, IfStatement):
|
|
78
|
+
self.visit(node.body, True)
|
|
79
|
+
for condition, body in node.elseifs:
|
|
80
|
+
self.visit(condition, True)
|
|
81
|
+
self.visit(body, True)
|
|
82
|
+
self.visit(node.else_body, True)
|
|
83
|
+
else:
|
|
84
|
+
self.visit(node.true_expr, True)
|
|
85
|
+
self.visit(node.false_expr, True)
|
|
86
|
+
|
|
87
|
+
def visit_switch(self, node, conditional: bool) -> None:
|
|
88
|
+
self.visit(node.expr, conditional)
|
|
89
|
+
for index, (case, body) in enumerate(node.cases):
|
|
90
|
+
self.visit(case, conditional if index == 0 else True)
|
|
91
|
+
self.visit(body, True)
|
|
92
|
+
self.visit(node.default, True)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _is_history_call(call: FunctionCall) -> bool:
|
|
96
|
+
return isinstance(call.func, str) and call.func.startswith(HISTORY_CALL_PREFIX)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Pipeline contracts: the abstractions every pine2pyne stage implements."""
|
|
2
|
+
from .contracts import (
|
|
3
|
+
AstNormalizer,
|
|
4
|
+
CodeEmitter,
|
|
5
|
+
CompilationUnit,
|
|
6
|
+
OutputProfile,
|
|
7
|
+
ScriptTransformer,
|
|
8
|
+
SemanticGuard,
|
|
9
|
+
SourceFilter,
|
|
10
|
+
SourceParser,
|
|
11
|
+
Transpiler,
|
|
12
|
+
TranspilerSettings,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
'AstNormalizer',
|
|
17
|
+
'CodeEmitter',
|
|
18
|
+
'CompilationUnit',
|
|
19
|
+
'OutputProfile',
|
|
20
|
+
'ScriptTransformer',
|
|
21
|
+
'SemanticGuard',
|
|
22
|
+
'SourceFilter',
|
|
23
|
+
'SourceParser',
|
|
24
|
+
'Transpiler',
|
|
25
|
+
'TranspilerSettings',
|
|
26
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Abstractions of the transpilation pipeline (the D in SOLID).
|
|
2
|
+
|
|
3
|
+
Every stage of pine2pyne is described here as a small Protocol. The
|
|
4
|
+
application service (:class:`pine2pyne.pipeline.service.TranspileService`)
|
|
5
|
+
depends only on these abstractions; concrete stages live in
|
|
6
|
+
:mod:`pine2pyne.pipeline.stages` and :mod:`pine2pyne.emit`, and are wired
|
|
7
|
+
together by the dishka container in :mod:`pine2pyne.di`.
|
|
8
|
+
|
|
9
|
+
Stages are deliberately narrow (the I in SOLID): a parser only parses, a
|
|
10
|
+
guard only refuses, an emitter only prints. A new output target is a new
|
|
11
|
+
:class:`CodeEmitter` implementation plus a provider, with no edit of the
|
|
12
|
+
service (the O in SOLID).
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import TYPE_CHECKING, Mapping, Protocol, runtime_checkable
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING: # the engine modules are heavy; contracts only name them
|
|
21
|
+
from ..ast_nodes import Script
|
|
22
|
+
from ..transformer import PyneTransformedScript
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OutputProfile(str, Enum):
|
|
26
|
+
"""Which compiler's output the emitter reproduces."""
|
|
27
|
+
|
|
28
|
+
#: byte-for-byte layout of the closed PyneComp v6.0.79 compiler (the default)
|
|
29
|
+
PYNECOMP = 'pynecomp'
|
|
30
|
+
#: the historical pine2pyne layout, kept as a reference
|
|
31
|
+
LEGACY = 'legacy'
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def parse(cls, value: 'OutputProfile | str | None') -> 'OutputProfile':
|
|
35
|
+
if value is None:
|
|
36
|
+
return cls.PYNECOMP
|
|
37
|
+
if isinstance(value, cls):
|
|
38
|
+
return value
|
|
39
|
+
try:
|
|
40
|
+
return cls(str(value).lower())
|
|
41
|
+
except ValueError:
|
|
42
|
+
names = ', '.join(profile.value for profile in cls)
|
|
43
|
+
raise ValueError(f'unknown output profile {value!r} (expected one of: {names})') from None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class TranspilerSettings:
|
|
48
|
+
"""Application-wide configuration, handed to the container as context."""
|
|
49
|
+
|
|
50
|
+
profile: OutputProfile = OutputProfile.PYNECOMP
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class CompilationUnit:
|
|
55
|
+
"""Everything an emitter may need about one script.
|
|
56
|
+
|
|
57
|
+
``pine_source`` and ``pine_script`` let an emitter recover source layout
|
|
58
|
+
(blank lines, quote style, the Pine version); ``transformed`` is the
|
|
59
|
+
PyneCore-shaped tree the code generator prints.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
pine_source: str
|
|
63
|
+
pine_script: 'Script'
|
|
64
|
+
transformed: 'PyneTransformedScript'
|
|
65
|
+
metadata: Mapping[str, object] = field(default_factory=dict)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def pine_version(self) -> int | None:
|
|
69
|
+
version = getattr(self.pine_script, 'version', None)
|
|
70
|
+
text = getattr(version, 'version', None)
|
|
71
|
+
return int(text) if text and str(text).isdigit() else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@runtime_checkable
|
|
75
|
+
class SourceFilter(Protocol):
|
|
76
|
+
"""Decides whether a source has anything to transpile at all."""
|
|
77
|
+
|
|
78
|
+
def is_blank(self, source: str) -> bool: ...
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@runtime_checkable
|
|
82
|
+
class SourceParser(Protocol):
|
|
83
|
+
"""Pine source text -> Pine AST (lexing, parsing and known repairs)."""
|
|
84
|
+
|
|
85
|
+
def parse(self, source: str) -> 'Script': ...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@runtime_checkable
|
|
89
|
+
class AstNormalizer(Protocol):
|
|
90
|
+
"""Pine AST -> Pine AST rewrite that keeps the program's meaning
|
|
91
|
+
(e.g. spelling Pine v1-v4 call names the way v5 spells them)."""
|
|
92
|
+
|
|
93
|
+
def normalize(self, script: 'Script') -> 'Script': ...
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@runtime_checkable
|
|
97
|
+
class SemanticGuard(Protocol):
|
|
98
|
+
"""Refuses what TradingView's compiler refuses; returns nothing."""
|
|
99
|
+
|
|
100
|
+
def check(self, script: 'Script') -> None: ...
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@runtime_checkable
|
|
104
|
+
class ScriptTransformer(Protocol):
|
|
105
|
+
"""Pine AST -> PyneCore-shaped tree."""
|
|
106
|
+
|
|
107
|
+
def transform(self, script: 'Script') -> 'PyneTransformedScript': ...
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@runtime_checkable
|
|
111
|
+
class CodeEmitter(Protocol):
|
|
112
|
+
"""PyneCore-shaped tree -> Python source text, in one output profile."""
|
|
113
|
+
|
|
114
|
+
profile: OutputProfile
|
|
115
|
+
|
|
116
|
+
def emit(self, unit: CompilationUnit) -> str: ...
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@runtime_checkable
|
|
120
|
+
class Transpiler(Protocol):
|
|
121
|
+
"""The use case: Pine source text -> Python source text."""
|
|
122
|
+
|
|
123
|
+
def transpile(self, source: str) -> str: ...
|
|
124
|
+
|
|
125
|
+
def validate(self, source: str) -> None: ...
|
pine2pyne/di.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Composition root: the dishka container that wires the pipeline.
|
|
2
|
+
|
|
3
|
+
Every concrete class is named here and nowhere else in the application
|
|
4
|
+
layer. A provider groups one responsibility; an output profile is one
|
|
5
|
+
provider, so a new target compiler is a new provider (open/closed)::
|
|
6
|
+
|
|
7
|
+
from pine2pyne.di import make_transpiler_container
|
|
8
|
+
from pine2pyne.core import OutputProfile, Transpiler
|
|
9
|
+
|
|
10
|
+
container = make_transpiler_container(OutputProfile.PYNECOMP)
|
|
11
|
+
print(container.get(Transpiler).transpile(pine_source))
|
|
12
|
+
container.close()
|
|
13
|
+
|
|
14
|
+
All stages are stateless (the single-use Transformer and CodeGenerator are
|
|
15
|
+
created per call inside their stages), so everything lives in ``Scope.APP``
|
|
16
|
+
and one container serves any number of scripts.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Sequence
|
|
21
|
+
|
|
22
|
+
from dishka import Container, Provider, Scope, from_context, make_container, provide
|
|
23
|
+
|
|
24
|
+
from .core.contracts import (
|
|
25
|
+
AstNormalizer,
|
|
26
|
+
CodeEmitter,
|
|
27
|
+
OutputProfile,
|
|
28
|
+
ScriptTransformer,
|
|
29
|
+
SemanticGuard,
|
|
30
|
+
SourceFilter,
|
|
31
|
+
SourceParser,
|
|
32
|
+
Transpiler,
|
|
33
|
+
TranspilerSettings,
|
|
34
|
+
)
|
|
35
|
+
from .emit.legacy import LegacyEmitter
|
|
36
|
+
from .emit.pynecomp import ImportBlockFormatter, PyneCompBanner, PyneCompEmitter, PyneCompModulePrinter
|
|
37
|
+
from .emit.pynecomp.passes import ModulePass, default_passes
|
|
38
|
+
from .pipeline.service import TranspileService
|
|
39
|
+
from .pipeline.stages import (
|
|
40
|
+
CommentOnlySourceFilter,
|
|
41
|
+
FreshTransformer,
|
|
42
|
+
InputHoistingNormalizer,
|
|
43
|
+
LegacyFunctionNameNormalizer,
|
|
44
|
+
LegacySessionNormalizer,
|
|
45
|
+
PineSemanticGuard,
|
|
46
|
+
RepairingSourceParser,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SettingsProvider(Provider):
|
|
51
|
+
"""Configuration handed in by the caller as container context."""
|
|
52
|
+
|
|
53
|
+
settings = from_context(provides=TranspilerSettings, scope=Scope.APP)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FrontEndProvider(Provider):
|
|
57
|
+
"""Pine text -> checked Pine AST."""
|
|
58
|
+
|
|
59
|
+
scope = Scope.APP
|
|
60
|
+
|
|
61
|
+
source_filter = provide(CommentOnlySourceFilter, provides=SourceFilter)
|
|
62
|
+
parser = provide(RepairingSourceParser, provides=SourceParser)
|
|
63
|
+
guard = provide(PineSemanticGuard, provides=SemanticGuard)
|
|
64
|
+
|
|
65
|
+
@provide
|
|
66
|
+
def normalizers(self) -> Sequence[AstNormalizer]:
|
|
67
|
+
return (LegacyFunctionNameNormalizer(), LegacySessionNormalizer(), InputHoistingNormalizer())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class MiddleEndProvider(Provider):
|
|
71
|
+
"""Pine AST -> PyneCore-shaped tree."""
|
|
72
|
+
|
|
73
|
+
scope = Scope.APP
|
|
74
|
+
|
|
75
|
+
transformer = provide(FreshTransformer, provides=ScriptTransformer)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class LegacyOutputProvider(Provider):
|
|
79
|
+
"""pine2pyne's own layout."""
|
|
80
|
+
|
|
81
|
+
scope = Scope.APP
|
|
82
|
+
|
|
83
|
+
emitter = provide(LegacyEmitter, provides=CodeEmitter)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class PyneCompOutputProvider(Provider):
|
|
87
|
+
"""The layout of the closed PyneComp v6.0.79 compiler."""
|
|
88
|
+
|
|
89
|
+
scope = Scope.APP
|
|
90
|
+
|
|
91
|
+
@provide
|
|
92
|
+
def banner(self) -> PyneCompBanner:
|
|
93
|
+
return PyneCompBanner()
|
|
94
|
+
|
|
95
|
+
@provide
|
|
96
|
+
def imports(self) -> ImportBlockFormatter:
|
|
97
|
+
return ImportBlockFormatter()
|
|
98
|
+
|
|
99
|
+
@provide
|
|
100
|
+
def printer(self, banner: PyneCompBanner, imports: ImportBlockFormatter) -> PyneCompModulePrinter:
|
|
101
|
+
return PyneCompModulePrinter(banner=banner, imports=imports)
|
|
102
|
+
|
|
103
|
+
@provide
|
|
104
|
+
def passes(self) -> Sequence[ModulePass]:
|
|
105
|
+
return default_passes()
|
|
106
|
+
|
|
107
|
+
@provide
|
|
108
|
+
def emitter(self, printer: PyneCompModulePrinter, passes: Sequence[ModulePass]) -> CodeEmitter:
|
|
109
|
+
return PyneCompEmitter(printer, passes)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ApplicationProvider(Provider):
|
|
113
|
+
"""The use case, assembled from whatever the other providers bound."""
|
|
114
|
+
|
|
115
|
+
scope = Scope.APP
|
|
116
|
+
|
|
117
|
+
@provide
|
|
118
|
+
def transpiler(
|
|
119
|
+
self,
|
|
120
|
+
source_filter: SourceFilter,
|
|
121
|
+
parser: SourceParser,
|
|
122
|
+
normalizers: Sequence[AstNormalizer],
|
|
123
|
+
guard: SemanticGuard,
|
|
124
|
+
transformer: ScriptTransformer,
|
|
125
|
+
emitter: CodeEmitter,
|
|
126
|
+
) -> Transpiler:
|
|
127
|
+
return TranspileService(source_filter, parser, normalizers, guard, transformer, emitter)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
#: The output provider of each profile. Register a new profile here.
|
|
131
|
+
OUTPUT_PROVIDERS: dict[OutputProfile, type[Provider]] = {
|
|
132
|
+
OutputProfile.LEGACY: LegacyOutputProvider,
|
|
133
|
+
OutputProfile.PYNECOMP: PyneCompOutputProvider,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def build_providers(profile: OutputProfile) -> list[Provider]:
|
|
138
|
+
return [
|
|
139
|
+
SettingsProvider(),
|
|
140
|
+
FrontEndProvider(),
|
|
141
|
+
MiddleEndProvider(),
|
|
142
|
+
OUTPUT_PROVIDERS[profile](),
|
|
143
|
+
ApplicationProvider(),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def make_transpiler_container(
|
|
148
|
+
profile: OutputProfile | str | None = None,
|
|
149
|
+
*extra_providers: Provider,
|
|
150
|
+
) -> Container:
|
|
151
|
+
"""A container for one output profile. ``extra_providers`` come last, so
|
|
152
|
+
they may override any binding (tests swap a stage this way)."""
|
|
153
|
+
resolved = OutputProfile.parse(profile)
|
|
154
|
+
return make_container(
|
|
155
|
+
*build_providers(resolved), *extra_providers,
|
|
156
|
+
context={TranspilerSettings: TranspilerSettings(profile=resolved)},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
_containers: dict[OutputProfile, Container] = {}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def default_container(profile: OutputProfile | str | None = None) -> Container:
|
|
164
|
+
"""The process-wide container of a profile, built on first use."""
|
|
165
|
+
resolved = OutputProfile.parse(profile)
|
|
166
|
+
container = _containers.get(resolved)
|
|
167
|
+
if container is None:
|
|
168
|
+
container = _containers.setdefault(resolved, make_transpiler_container(resolved))
|
|
169
|
+
return container
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def get_transpiler(profile: OutputProfile | str | None = None) -> Transpiler:
|
|
173
|
+
return default_container(profile).get(Transpiler)
|