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,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)