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,422 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type inference for Pine Script variables.
|
|
3
|
+
|
|
4
|
+
Determines whether variables should be Series[T] or Persistent[T] in PyneCore.
|
|
5
|
+
"""
|
|
6
|
+
from typing import AbstractSet, Dict, List, Optional, Set
|
|
7
|
+
from .ast_nodes import *
|
|
8
|
+
from .symbol_table import SymbolTable, Symbol, VariableKind
|
|
9
|
+
from .bool_builtins import BOOL_BUILTINS
|
|
10
|
+
from .pine_builtins import get_type_name
|
|
11
|
+
|
|
12
|
+
TYPE_WRAPPERS = ('Persistent', 'Series', 'PersistentSeries')
|
|
13
|
+
# The color functions that return a colour; color.r/g/b/t return numbers
|
|
14
|
+
COLOUR_CONSTRUCTORS = frozenset({'color.new', 'color.rgb', 'color.from_gradient'})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def receiver_path(receiver) -> Optional[List[str]]:
|
|
18
|
+
"""Names along a receiver: `t.inner.bx` -> ['t', 'inner', 'bx'].
|
|
19
|
+
|
|
20
|
+
A history reference keeps its base's type (`b[1]` is still a box), so it
|
|
21
|
+
reduces to the path of its object. Any other expression has no path.
|
|
22
|
+
"""
|
|
23
|
+
if isinstance(receiver, str):
|
|
24
|
+
return receiver.split('.')
|
|
25
|
+
if isinstance(receiver, Identifier):
|
|
26
|
+
return receiver.name.split('.')
|
|
27
|
+
if isinstance(receiver, IndexAccess):
|
|
28
|
+
return receiver_path(receiver.object)
|
|
29
|
+
if isinstance(receiver, MemberAccess):
|
|
30
|
+
object_path = receiver_path(receiver.object)
|
|
31
|
+
return object_path + [receiver.member] if object_path else None
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TypeInference:
|
|
36
|
+
"""Analyzes AST to infer variable types (Series vs Persistent vs simple)."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, symbol_table: SymbolTable):
|
|
39
|
+
self.symbol_table = symbol_table
|
|
40
|
+
self.ta_functions = self._init_ta_functions()
|
|
41
|
+
# (local types, shadowed names) of the scope being transformed; the
|
|
42
|
+
# transformer replaces this so a dotted value is typed where it appears
|
|
43
|
+
self.current_scope = lambda: ({}, frozenset())
|
|
44
|
+
|
|
45
|
+
def _init_ta_functions(self) -> Set[str]:
|
|
46
|
+
"""Initialize complete set of ta.* functions/variables (Pine v5/v6)."""
|
|
47
|
+
return {
|
|
48
|
+
# Moving averages
|
|
49
|
+
'ta.sma', 'ta.ema', 'ta.wma', 'ta.vwma', 'ta.swma', 'ta.alma',
|
|
50
|
+
'ta.rma', 'ta.hma', 'ta.linreg',
|
|
51
|
+
# Oscillators & indicators
|
|
52
|
+
'ta.rsi', 'ta.macd', 'ta.stoch', 'ta.cci', 'ta.cmo', 'ta.mfi',
|
|
53
|
+
'ta.mom', 'ta.roc', 'ta.tsi', 'ta.wpr',
|
|
54
|
+
# Volatility
|
|
55
|
+
'ta.atr', 'ta.tr', 'ta.bb', 'ta.bbw', 'ta.kc', 'ta.kcw',
|
|
56
|
+
# Trend
|
|
57
|
+
'ta.dmi', 'ta.supertrend', 'ta.sar', 'ta.cog',
|
|
58
|
+
# Volume-based (functions and built-in variables)
|
|
59
|
+
'ta.obv', 'ta.nvi', 'ta.pvi', 'ta.pvt', 'ta.vwap',
|
|
60
|
+
'ta.accdist', 'ta.iii', 'ta.wad', 'ta.wvad',
|
|
61
|
+
# Statistical
|
|
62
|
+
'ta.dev', 'ta.stdev', 'ta.variance', 'ta.correlation',
|
|
63
|
+
'ta.median', 'ta.mode', 'ta.range', 'ta.cum',
|
|
64
|
+
'ta.percentile_linear_interpolation', 'ta.percentile_nearest_rank',
|
|
65
|
+
'ta.percentrank',
|
|
66
|
+
# Lookback
|
|
67
|
+
'ta.highest', 'ta.lowest', 'ta.highestbars', 'ta.lowestbars',
|
|
68
|
+
'ta.valuewhen', 'ta.barssince', 'ta.change',
|
|
69
|
+
# Pivot
|
|
70
|
+
'ta.pivothigh', 'ta.pivotlow',
|
|
71
|
+
# Boolean series
|
|
72
|
+
'ta.rising', 'ta.falling', 'ta.cross', 'ta.crossover', 'ta.crossunder',
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def infer_types(self, script: Script) -> None:
|
|
76
|
+
"""Infer types for all variables in the script."""
|
|
77
|
+
# First pass: mark variables with var/varip as Persistent
|
|
78
|
+
self._mark_persistent_variables(script)
|
|
79
|
+
|
|
80
|
+
# Second pass: mark variables from ta.* functions as Series
|
|
81
|
+
self._mark_ta_series_variables(script)
|
|
82
|
+
|
|
83
|
+
# Third pass: mark indexed variables as Series
|
|
84
|
+
self._mark_indexed_variables(script)
|
|
85
|
+
|
|
86
|
+
# Fourth pass: mark global non-var variables as Series (PRIMARY RULE)
|
|
87
|
+
self._mark_global_series_variables(script)
|
|
88
|
+
|
|
89
|
+
# Fifth pass: propagate Series type from assignments
|
|
90
|
+
self._propagate_series_types(script)
|
|
91
|
+
|
|
92
|
+
def _mark_persistent_variables(self, script: Script) -> None:
|
|
93
|
+
"""Mark all var/varip variables as Persistent."""
|
|
94
|
+
for decl in script.declarations:
|
|
95
|
+
if isinstance(decl, (VarDecl, VaripDecl)):
|
|
96
|
+
symbol = self.symbol_table.lookup(decl.name)
|
|
97
|
+
if symbol:
|
|
98
|
+
# These are already marked as VAR/VARIP in symbol table
|
|
99
|
+
symbol.is_series = False # Not Series, will be Persistent
|
|
100
|
+
|
|
101
|
+
def _mark_ta_series_variables(self, script: Script) -> None:
|
|
102
|
+
"""Mark variables assigned from ta.* functions as Series."""
|
|
103
|
+
# Check global assignments
|
|
104
|
+
for stmt in script.body:
|
|
105
|
+
if isinstance(stmt, Assignment):
|
|
106
|
+
if self._is_ta_function_call(stmt.value):
|
|
107
|
+
if isinstance(stmt.target, str):
|
|
108
|
+
self.symbol_table.mark_from_ta_function(stmt.target)
|
|
109
|
+
|
|
110
|
+
def _mark_indexed_variables(self, script: Script) -> None:
|
|
111
|
+
"""Mark variables that are indexed (e.g., close[1]) as Series."""
|
|
112
|
+
self.indexed_var_names: set[str] = set()
|
|
113
|
+
self._find_indexed_in_node(script)
|
|
114
|
+
|
|
115
|
+
def _find_indexed_in_node(self, node: Any) -> None:
|
|
116
|
+
"""Recursively find indexed access patterns."""
|
|
117
|
+
if isinstance(node, IndexAccess):
|
|
118
|
+
if isinstance(node.object, Identifier):
|
|
119
|
+
self.indexed_var_names.add(node.object.name)
|
|
120
|
+
self.symbol_table.mark_as_indexed(node.object.name)
|
|
121
|
+
|
|
122
|
+
# Recursively check all child nodes. dict must be traversed too:
|
|
123
|
+
# function-call keyword arguments are stored as a dict, so a history
|
|
124
|
+
# access like `x[1]` inside a kwarg (e.g. plot(color=(x >= x[1] ? ...)))
|
|
125
|
+
# would otherwise be invisible and x would not be promoted to Series.
|
|
126
|
+
if isinstance(node, dict):
|
|
127
|
+
for value in node.values():
|
|
128
|
+
self._find_indexed_in_node(value)
|
|
129
|
+
elif isinstance(node, (list, tuple)):
|
|
130
|
+
for item in node:
|
|
131
|
+
self._find_indexed_in_node(item)
|
|
132
|
+
elif hasattr(node, '__dict__'):
|
|
133
|
+
for attr_value in node.__dict__.values():
|
|
134
|
+
if isinstance(attr_value, (ASTNode, list, tuple, dict)):
|
|
135
|
+
self._find_indexed_in_node(attr_value)
|
|
136
|
+
|
|
137
|
+
def _mark_global_series_variables(self, script: Script) -> None:
|
|
138
|
+
"""Mark all global non-var, non-input variables as Series (PRIMARY RULE)."""
|
|
139
|
+
for symbol in self.symbol_table.get_all_globals():
|
|
140
|
+
# Skip var/varip (they're Persistent)
|
|
141
|
+
if symbol.kind in (VariableKind.VAR, VariableKind.VARIP):
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
# Skip inputs (they become function parameters)
|
|
145
|
+
if symbol.kind == VariableKind.INPUT:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# Skip functions
|
|
149
|
+
if symbol.kind == VariableKind.FUNCTION:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# Everything else is Series by default
|
|
153
|
+
if symbol.is_global:
|
|
154
|
+
symbol.is_series = True
|
|
155
|
+
|
|
156
|
+
def _propagate_series_types(self, script: Script) -> None:
|
|
157
|
+
"""Propagate Series type from one variable to another in assignments."""
|
|
158
|
+
for stmt in script.body:
|
|
159
|
+
if isinstance(stmt, Assignment):
|
|
160
|
+
if isinstance(stmt.target, str):
|
|
161
|
+
# Check if value is a Series variable
|
|
162
|
+
if isinstance(stmt.value, Identifier):
|
|
163
|
+
source_symbol = self.symbol_table.lookup(stmt.value.name)
|
|
164
|
+
if source_symbol and source_symbol.is_series:
|
|
165
|
+
self.symbol_table.mark_as_series(stmt.target)
|
|
166
|
+
|
|
167
|
+
def _is_ta_function_call(self, expr: Expression) -> bool:
|
|
168
|
+
"""Check if expression is a ta.* function call."""
|
|
169
|
+
if isinstance(expr, FunctionCall):
|
|
170
|
+
if isinstance(expr.func, str):
|
|
171
|
+
return expr.func in self.ta_functions
|
|
172
|
+
elif isinstance(expr.func, MemberAccess):
|
|
173
|
+
func_name = f"{expr.func.object}.{expr.func.member}"
|
|
174
|
+
return func_name in self.ta_functions
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
def infer_type_hint(self, value: Expression) -> str:
|
|
178
|
+
"""Infer the Python type hint for an expression."""
|
|
179
|
+
if isinstance(value, Literal):
|
|
180
|
+
type_map = {
|
|
181
|
+
'int': 'int',
|
|
182
|
+
'float': 'float',
|
|
183
|
+
'string': 'str',
|
|
184
|
+
'bool': 'bool',
|
|
185
|
+
'color': 'Color',
|
|
186
|
+
}
|
|
187
|
+
return type_map.get(value.literal_type, 'Any')
|
|
188
|
+
|
|
189
|
+
if isinstance(value, NaLiteral):
|
|
190
|
+
return 'Any' # NA can be any type
|
|
191
|
+
|
|
192
|
+
if isinstance(value, UnaryOp):
|
|
193
|
+
if value.op == 'not':
|
|
194
|
+
return 'bool'
|
|
195
|
+
# Unary +/- preserves operand type
|
|
196
|
+
return self.infer_type_hint(value.operand)
|
|
197
|
+
|
|
198
|
+
if isinstance(value, BinaryOp):
|
|
199
|
+
# Boolean operations always return bool
|
|
200
|
+
if value.op in ('and', 'or', '==', '!=', '>', '<', '>=', '<='):
|
|
201
|
+
return 'bool'
|
|
202
|
+
|
|
203
|
+
# Infer from operands
|
|
204
|
+
left_type = self.infer_type_hint(value.left)
|
|
205
|
+
right_type = self.infer_type_hint(value.right)
|
|
206
|
+
|
|
207
|
+
# If either is float, result is float
|
|
208
|
+
if left_type == 'float' or right_type == 'float':
|
|
209
|
+
return 'float'
|
|
210
|
+
|
|
211
|
+
# If both are int, result is int (except for division)
|
|
212
|
+
if left_type == 'int' and right_type == 'int':
|
|
213
|
+
if value.op == '/':
|
|
214
|
+
return 'float'
|
|
215
|
+
return 'int'
|
|
216
|
+
|
|
217
|
+
return 'Any'
|
|
218
|
+
|
|
219
|
+
if isinstance(value, TernaryOp):
|
|
220
|
+
# Type is union of true and false branches
|
|
221
|
+
true_type = self.infer_type_hint(value.true_expr)
|
|
222
|
+
false_type = self.infer_type_hint(value.false_expr)
|
|
223
|
+
if true_type == false_type:
|
|
224
|
+
return true_type
|
|
225
|
+
# If mixed int/float, return float
|
|
226
|
+
if {true_type, false_type} == {'int', 'float'}:
|
|
227
|
+
return 'float'
|
|
228
|
+
return 'Any'
|
|
229
|
+
|
|
230
|
+
if isinstance(value, FunctionCall):
|
|
231
|
+
# Check if it's a ta.* function
|
|
232
|
+
func_name = None
|
|
233
|
+
if isinstance(value.func, str):
|
|
234
|
+
func_name = value.func
|
|
235
|
+
elif isinstance(value.func, MemberAccess):
|
|
236
|
+
func_name = f"{value.func.object}.{value.func.member}"
|
|
237
|
+
|
|
238
|
+
if func_name in BOOL_BUILTINS:
|
|
239
|
+
return 'bool' # ta.crossover, str.contains, array.includes, ... (card #56)
|
|
240
|
+
if func_name and func_name in self.ta_functions:
|
|
241
|
+
return 'float' # Most ta.* functions return float series
|
|
242
|
+
|
|
243
|
+
# input.* functions
|
|
244
|
+
if func_name and func_name.startswith('input.'):
|
|
245
|
+
input_type_map = {
|
|
246
|
+
'input.int': 'int',
|
|
247
|
+
'input.float': 'float',
|
|
248
|
+
'input.bool': 'bool',
|
|
249
|
+
'input.string': 'str',
|
|
250
|
+
'input.color': 'Color',
|
|
251
|
+
'input.source': 'float', # Sources are float series
|
|
252
|
+
}
|
|
253
|
+
return input_type_map.get(func_name, 'Any')
|
|
254
|
+
|
|
255
|
+
# PyneCore collection constructors
|
|
256
|
+
if func_name:
|
|
257
|
+
# matrix.new<float>() -> Matrix[float]
|
|
258
|
+
if func_name.startswith('matrix.new'):
|
|
259
|
+
# Check for generic type in func_name: matrix.new<float>
|
|
260
|
+
if '<' in func_name and '>' in func_name:
|
|
261
|
+
generic = func_name[func_name.index('<')+1:func_name.index('>')]
|
|
262
|
+
return f'Matrix[{generic}]'
|
|
263
|
+
return 'Matrix[float]' # Default to float
|
|
264
|
+
|
|
265
|
+
# matrix.* methods that return a NEW Matrix (so a variable assigned
|
|
266
|
+
# from them, e.g. `t = m.transpose()`, is typed as a Matrix and its
|
|
267
|
+
# own method calls resolve to the matrix module). Two spellings
|
|
268
|
+
# reach here: the module form `matrix.transpose(m)` (after method
|
|
269
|
+
# resolution), and the pre-resolution method form `m.transpose()`
|
|
270
|
+
# which the parser stores as FunctionCall(func='m.transpose') — for
|
|
271
|
+
# the latter the object must be a known Matrix so the same method
|
|
272
|
+
# names on arrays/maps are not mistyped.
|
|
273
|
+
_MATRIX_RETURNING = ('transpose', 'copy', 'submatrix', 'mult', 'kron',
|
|
274
|
+
'pow', 'inv', 'pinv', 'reshape', 'diff')
|
|
275
|
+
if func_name.startswith('matrix.') and func_name.split('.', 1)[1] in _MATRIX_RETURNING:
|
|
276
|
+
return 'Matrix[float]'
|
|
277
|
+
if '.' in func_name:
|
|
278
|
+
_obj_name, _, _meth = func_name.rpartition('.')
|
|
279
|
+
if _meth in _MATRIX_RETURNING and '.' not in _obj_name:
|
|
280
|
+
_obj_symbol = self.symbol_table.lookup(_obj_name)
|
|
281
|
+
if _obj_symbol and _obj_symbol.type_hint and 'Matrix' in _obj_symbol.type_hint:
|
|
282
|
+
return 'Matrix[float]'
|
|
283
|
+
|
|
284
|
+
# array.new<T>() -> list[T]; array.from(...) -> list (card #8)
|
|
285
|
+
if func_name.startswith('array.new<') and func_name.endswith('>'):
|
|
286
|
+
return f"list[{get_type_name(func_name[len('array.new<'):-1].strip())}]"
|
|
287
|
+
if func_name == 'array.from':
|
|
288
|
+
# the arguments' common type (PyneComp: `array.from(1.0, 2.0)` is list[float])
|
|
289
|
+
elements = {self.infer_type_hint(argument) for argument in value.args}
|
|
290
|
+
if elements == {'int', 'float'}:
|
|
291
|
+
elements = {'float'}
|
|
292
|
+
if len(elements) == 1 and 'Any' not in elements and None not in elements:
|
|
293
|
+
return f'list[{elements.pop()}]'
|
|
294
|
+
return 'list[Any]'
|
|
295
|
+
|
|
296
|
+
# array.new_*() -> list[type]
|
|
297
|
+
if func_name.startswith('array.new_'):
|
|
298
|
+
array_type_map = {
|
|
299
|
+
'array.new_int': 'list[int]',
|
|
300
|
+
'array.new_float': 'list[float]',
|
|
301
|
+
'array.new_bool': 'list[bool]',
|
|
302
|
+
'array.new_string': 'list[str]',
|
|
303
|
+
'array.new_color': 'list[Color]',
|
|
304
|
+
'array.new_line': 'list[Line]',
|
|
305
|
+
'array.new_label': 'list[Label]',
|
|
306
|
+
'array.new_box': 'list[Box]',
|
|
307
|
+
'array.new_table': 'list[Table]',
|
|
308
|
+
}
|
|
309
|
+
return array_type_map.get(func_name, 'list[Any]')
|
|
310
|
+
|
|
311
|
+
# map.new<K,V>() -> dict[K, V]
|
|
312
|
+
if func_name.startswith('map.new'):
|
|
313
|
+
if '<' in func_name and '>' in func_name:
|
|
314
|
+
generic = func_name[func_name.index('<')+1:func_name.index('>')]
|
|
315
|
+
parts = [p.strip() for p in generic.split(',')]
|
|
316
|
+
if len(parts) == 2:
|
|
317
|
+
k = get_type_name(parts[0])
|
|
318
|
+
v = get_type_name(parts[1])
|
|
319
|
+
return f'dict[{k}, {v}]'
|
|
320
|
+
return 'dict[str, float]' # Default generic
|
|
321
|
+
|
|
322
|
+
# map.get -> float (most common map value type in Pine strategies)
|
|
323
|
+
if func_name == 'map.get':
|
|
324
|
+
return 'float'
|
|
325
|
+
|
|
326
|
+
# map.copy(m) / map.keys(m) / map.values(m) -> same type
|
|
327
|
+
if func_name == 'map.copy':
|
|
328
|
+
return 'dict[str, float]' # Copy returns a map
|
|
329
|
+
if func_name in ('map.keys', 'array.copy'):
|
|
330
|
+
return 'list[Any]'
|
|
331
|
+
if func_name == 'map.values':
|
|
332
|
+
return 'list[Any]'
|
|
333
|
+
|
|
334
|
+
# label.new(), line.new(), box.new(), table.new()
|
|
335
|
+
if func_name in ('label.new', 'line.new', 'box.new', 'table.new'):
|
|
336
|
+
type_name = func_name.split('.')[0].capitalize()
|
|
337
|
+
return type_name
|
|
338
|
+
|
|
339
|
+
# Check if it's a UDT constructor call (e.g., TradeStats())
|
|
340
|
+
if isinstance(value.func, str):
|
|
341
|
+
# Check if the function name is a known UDT type
|
|
342
|
+
symbol = self.symbol_table.lookup(value.func)
|
|
343
|
+
if symbol and symbol.kind == VariableKind.TYPE:
|
|
344
|
+
return value.func # Return the UDT type name
|
|
345
|
+
# Fallback: if function name starts with uppercase, assume it's a UDT
|
|
346
|
+
elif value.func and value.func[0].isupper():
|
|
347
|
+
return value.func
|
|
348
|
+
|
|
349
|
+
# Handle MethodCall (e.g., TradeStats.new() before transformation)
|
|
350
|
+
if isinstance(value, MethodCall):
|
|
351
|
+
# Check if it's UDT.new() pattern
|
|
352
|
+
if value.method == 'new' and isinstance(value.object, Identifier):
|
|
353
|
+
# Check if the object is a UDT type
|
|
354
|
+
if value.object.name[0].isupper():
|
|
355
|
+
return value.object.name # Return just the UDT name without .new suffix
|
|
356
|
+
|
|
357
|
+
# Handle FunctionCall for UDT constructors (after transformation: TradeStats())
|
|
358
|
+
if isinstance(value, FunctionCall):
|
|
359
|
+
if isinstance(value.func, str) and value.func and value.func[0].isupper():
|
|
360
|
+
# Already a constructor call - return the type name directly
|
|
361
|
+
return value.func
|
|
362
|
+
|
|
363
|
+
if isinstance(value, Identifier):
|
|
364
|
+
symbol = self.symbol_table.lookup(value.name)
|
|
365
|
+
if symbol and symbol.type_hint:
|
|
366
|
+
return symbol.type_hint
|
|
367
|
+
|
|
368
|
+
# A UDT field (`b = t.bx`) has the field's declared type
|
|
369
|
+
if isinstance(value, (Identifier, MemberAccess)):
|
|
370
|
+
return self.infer_receiver_type(value, *self.current_scope()) or 'Any'
|
|
371
|
+
|
|
372
|
+
return 'Any'
|
|
373
|
+
|
|
374
|
+
def is_colour_value(self, value: Expression) -> bool:
|
|
375
|
+
"""True for a colour: a colour literal or `color.*` constant, a colour
|
|
376
|
+
constructor, or a ternary whose arms are colours (an arm may be na)."""
|
|
377
|
+
if isinstance(value, Literal):
|
|
378
|
+
return value.literal_type == 'color'
|
|
379
|
+
if isinstance(value, Identifier):
|
|
380
|
+
return value.name.startswith('color.')
|
|
381
|
+
if isinstance(value, FunctionCall):
|
|
382
|
+
return isinstance(value.func, str) and value.func in COLOUR_CONSTRUCTORS
|
|
383
|
+
if isinstance(value, TernaryOp):
|
|
384
|
+
arms = [value.true_expr, value.false_expr]
|
|
385
|
+
return (any(self.is_colour_value(arm) for arm in arms)
|
|
386
|
+
and all(isinstance(arm, NaLiteral) or self.is_colour_value(arm) for arm in arms))
|
|
387
|
+
return False
|
|
388
|
+
|
|
389
|
+
def infer_receiver_type(self, receiver, local_types: Dict[str, str],
|
|
390
|
+
shadowed_names: AbstractSet[str] = frozenset()) -> Optional[str]:
|
|
391
|
+
"""Declared type of a method-call receiver, or None when it is not known.
|
|
392
|
+
|
|
393
|
+
The base name is typed from `local_types` (the current function's
|
|
394
|
+
parameters and locals), then the symbol table; each further name is a
|
|
395
|
+
UDT field typed from its `type` declaration, to any depth. A field path
|
|
396
|
+
whose base is a function local shadowing a global is left unknown: the
|
|
397
|
+
transformer records the global's type for such a local; so is the base itself
|
|
398
|
+
unless the current scope types it (N1, card #8).
|
|
399
|
+
"""
|
|
400
|
+
path = receiver_path(receiver)
|
|
401
|
+
if not path:
|
|
402
|
+
return None
|
|
403
|
+
base_name, *field_names = path
|
|
404
|
+
if base_name in shadowed_names and base_name not in local_types:
|
|
405
|
+
return None
|
|
406
|
+
receiver_type = local_types.get(base_name)
|
|
407
|
+
if receiver_type is None:
|
|
408
|
+
symbol = self.symbol_table.lookup(base_name)
|
|
409
|
+
receiver_type = symbol.type_hint if symbol else None
|
|
410
|
+
for field_name in field_names:
|
|
411
|
+
field_types = self.symbol_table.udt_field_types.get(self.declared_type_name(receiver_type))
|
|
412
|
+
receiver_type = field_types.get(field_name) if field_types else None
|
|
413
|
+
return self.declared_type_name(receiver_type)
|
|
414
|
+
|
|
415
|
+
def declared_type_name(self, type_hint: Optional[str]) -> Optional[str]:
|
|
416
|
+
"""`Persistent[Box]` -> 'Box'; a UDT constructor type `T.new` -> 'T';
|
|
417
|
+
None for an unknown or untyped ('Any', bare 'Series') value."""
|
|
418
|
+
while type_hint and type_hint.endswith(']') and type_hint.split('[', 1)[0] in TYPE_WRAPPERS:
|
|
419
|
+
type_hint = type_hint[type_hint.index('[') + 1:-1]
|
|
420
|
+
if type_hint and type_hint.endswith('.new') and type_hint[:-len('.new')] in self.symbol_table.udt_field_types:
|
|
421
|
+
type_hint = type_hint[:-len('.new')]
|
|
422
|
+
return None if type_hint in (None, '', 'Any', *TYPE_WRAPPERS) else type_hint
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UDT field defaults (card #17).
|
|
3
|
+
|
|
4
|
+
Pine initializes a field with its default "each time that the script creates a
|
|
5
|
+
new instance of the UDT"; a field without a default is na, or false for a bool
|
|
6
|
+
in Pine v6 (v5 gives a bool field na too, card #76).
|
|
7
|
+
"The default value cannot be the result of a function call or any other
|
|
8
|
+
expression; only a literal value or a compatible built-in variable is allowed"
|
|
9
|
+
(Pine v6 manual, User-defined types).
|
|
10
|
+
|
|
11
|
+
So a default is one of:
|
|
12
|
+
- a literal (`1.5`, `-1.5`, `"a"`, `#ff0000`, `true`, `na`) or a namespace or
|
|
13
|
+
enum constant (`color.red`, `xloc.bar_time`, `syminfo.mintick`, `Mode.a`):
|
|
14
|
+
the same on every `.new()`, so it stays a class attribute, as before;
|
|
15
|
+
- an undotted series built-in variable (`open`, `bar_index`, `time`, ...,
|
|
16
|
+
pine_builtins.SERIES_VARIABLES): it changes from bar to bar, so it is read
|
|
17
|
+
per instance, by `field(default_factory=...)`; so is a dotted variable on the
|
|
18
|
+
DYNAMIC_DOTTED_DEFAULTS allowlist (`barstate.isfirst`, `strategy.position_size`,
|
|
19
|
+
card #80). A dotted constant, a function and a variable-function (`ta.tr`) are
|
|
20
|
+
not on it: they stay class attributes.
|
|
21
|
+
Anything else is refused here, as TradingView refuses it: a SemanticError
|
|
22
|
+
('udt-field-default') at the default's Pine line and column (card #28), instead
|
|
23
|
+
of failing at load.
|
|
24
|
+
"""
|
|
25
|
+
from typing import AbstractSet, FrozenSet, List, Optional, Tuple
|
|
26
|
+
|
|
27
|
+
from .ast_nodes import EnumDecl, Identifier, Literal, NaLiteral, Script, TypeDecl, UnaryOp
|
|
28
|
+
from .ast_walk import walk
|
|
29
|
+
from .errors import SemanticError
|
|
30
|
+
from .pine_builtins import SERIES_VARIABLES
|
|
31
|
+
from .scope_renamer import NAMESPACE_HEADS
|
|
32
|
+
|
|
33
|
+
DEFAULT_RULE = ('Pine allows only a literal value or a compatible built-in variable as a field default '
|
|
34
|
+
'(Pine v6 manual, User-defined types)')
|
|
35
|
+
# dataclasses.field under a name no UDT field, type or enum is likely to take:
|
|
36
|
+
# a class-body name `field` would shadow a bare import
|
|
37
|
+
FIELD_FACTORY = '_dataclass_field'
|
|
38
|
+
FIELD_FACTORY_IMPORT = f'from dataclasses import field as {FIELD_FACTORY}'
|
|
39
|
+
|
|
40
|
+
# Dotted built-in variables that change while the script runs, so a UDT field default that
|
|
41
|
+
# names one is read on every `.new()` (card #80). A curated list, not a namespace prefix: a
|
|
42
|
+
# namespace also holds constants (`session.regular`, `strategy.fixed`) and functions
|
|
43
|
+
# (`strategy.entry`, `ta.sma`), and a variable-function such as `ta.tr` is a call, not a read.
|
|
44
|
+
DYNAMIC_DOTTED_DEFAULTS: FrozenSet[str] = frozenset(
|
|
45
|
+
{f'barstate.{member}' for member in (
|
|
46
|
+
'isconfirmed', 'isfirst', 'ishistory', 'islast', 'islastconfirmedhistory', 'isnew', 'isrealtime')}
|
|
47
|
+
| {f'session.{member}' for member in (
|
|
48
|
+
'isfirstbar', 'isfirstbar_regular', 'islastbar', 'islastbar_regular', 'ismarket', 'ispostmarket',
|
|
49
|
+
'ispremarket')}
|
|
50
|
+
| {f'strategy.{member}' for member in (
|
|
51
|
+
'avg_losing_trade', 'avg_losing_trade_percent', 'avg_trade', 'avg_trade_percent', 'avg_winning_trade',
|
|
52
|
+
'avg_winning_trade_percent', 'closedtrades', 'equity', 'eventrades', 'grossloss', 'grossloss_percent',
|
|
53
|
+
'grossprofit', 'grossprofit_percent', 'losstrades', 'margin_liquidation_price', 'max_contracts_held_all',
|
|
54
|
+
'max_contracts_held_long', 'max_contracts_held_short', 'max_drawdown', 'max_drawdown_percent',
|
|
55
|
+
'max_runup', 'max_runup_percent', 'netprofit', 'netprofit_percent', 'openprofit', 'openprofit_percent',
|
|
56
|
+
'opentrades', 'position_avg_price', 'position_entry_name', 'position_size', 'wintrades')}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_series_default(default) -> bool:
|
|
61
|
+
"""A default that is a series built-in variable, undotted or on the dotted allowlist:
|
|
62
|
+
read on every `.new()`. The one predicate field emission and import discovery share."""
|
|
63
|
+
return isinstance(default, Identifier) and (default.name in SERIES_VARIABLES
|
|
64
|
+
or default.name in DYNAMIC_DOTTED_DEFAULTS)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def constant_heads(script: Script) -> FrozenSet[str]:
|
|
68
|
+
"""The heads a dotted constant default may have: the built-in namespaces,
|
|
69
|
+
the script's enums and its imported libraries."""
|
|
70
|
+
return frozenset(NAMESPACE_HEADS | {decl.name for decl in script.declarations if isinstance(decl, EnumDecl)}
|
|
71
|
+
| {library.alias or library.library for library in script.imports})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def invalid_defaults(type_decl: TypeDecl, heads: AbstractSet[str]) -> List[Tuple[str, object]]:
|
|
75
|
+
"""(field name, default) of each default that is neither a literal, a
|
|
76
|
+
constant nor a built-in variable. Read by pine_semantics too, so
|
|
77
|
+
`--validate` refuses what transpile() refuses (card #28)."""
|
|
78
|
+
return [(field_name, default) for field_name, _, default in type_decl.fields
|
|
79
|
+
if default is not None and not _is_allowed_default(default, heads)]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def default_message(type_decl: TypeDecl, field_name: str) -> str:
|
|
83
|
+
return f"default of field '{type_decl.name}.{field_name}': {DEFAULT_RULE}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def positioned_part(default):
|
|
87
|
+
"""The node whose position a refused default reports: the default, else its
|
|
88
|
+
first part that has a column (an operator carries only its field's line)."""
|
|
89
|
+
return next((node for node in walk(default) if getattr(node, 'column', 0)), default)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def refuse_invalid_defaults(type_decl: TypeDecl, heads: AbstractSet[str]) -> None:
|
|
93
|
+
"""Raise SemanticError('udt-field-default') for the first default Pine forbids."""
|
|
94
|
+
for field_name, default in invalid_defaults(type_decl, heads)[:1]:
|
|
95
|
+
at = positioned_part(default)
|
|
96
|
+
raise SemanticError('udt-field-default', default_message(type_decl, field_name),
|
|
97
|
+
getattr(at, 'line', 0), getattr(at, 'column', 0))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _is_allowed_default(default, constant_heads: AbstractSet[str]) -> bool:
|
|
101
|
+
if isinstance(default, (Literal, NaLiteral)):
|
|
102
|
+
return True
|
|
103
|
+
if isinstance(default, UnaryOp):
|
|
104
|
+
return default.op in ('-', '+') and isinstance(default.operand, Literal)
|
|
105
|
+
if not isinstance(default, Identifier):
|
|
106
|
+
return False
|
|
107
|
+
head, dot, _ = default.name.partition('.')
|
|
108
|
+
return head in constant_heads if dot else default.name in SERIES_VARIABLES
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def emitted_default_requires_na(py_type: str, default, bool_is_never_na: bool) -> bool:
|
|
112
|
+
"""True when `field_value` declares the field with `na` or `NA(...)`, so the
|
|
113
|
+
script must import them. The one predicate the import resolver and the
|
|
114
|
+
emitter share (card #76). A bool field with no default is false in Pine v6
|
|
115
|
+
(`bool_is_never_na`) and na before it."""
|
|
116
|
+
if isinstance(default, NaLiteral):
|
|
117
|
+
return True
|
|
118
|
+
return default is None and not (py_type == 'bool' and bool_is_never_na)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def field_value(py_type: str, na_type: str, default, generated_default: Optional[str],
|
|
122
|
+
of_its_own_class: bool, bool_is_never_na: bool = True) -> str:
|
|
123
|
+
"""The Python value a UDT field is declared with. `of_its_own_class`: the
|
|
124
|
+
field holds an instance of a UDT (its own class or another), made per instance.
|
|
125
|
+
A series built-in or any dotted constant (`color.black`, `xloc.bar_index`, an
|
|
126
|
+
enum member) is read through a factory too, as PyneComp writes it (the reference:
|
|
127
|
+
163 such fields, none written plain)."""
|
|
128
|
+
if default is None or isinstance(default, NaLiteral):
|
|
129
|
+
return _na_value(py_type, na_type, default is None, of_its_own_class, bool_is_never_na)
|
|
130
|
+
if is_series_default(default) or is_dotted_default(default):
|
|
131
|
+
return f'{FIELD_FACTORY}(default_factory=lambda: {generated_default})'
|
|
132
|
+
return generated_default
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def is_dotted_default(default) -> bool:
|
|
136
|
+
"""`color.black`, `xloc.bar_index`, `Mode.a`: a member of a namespace or an enum."""
|
|
137
|
+
from .ast_nodes import MemberAccess
|
|
138
|
+
return (isinstance(default, Identifier) and '.' in default.name) or isinstance(default, MemberAccess)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _na_value(py_type: str, na_type: str, no_default: bool, of_its_own_class: bool,
|
|
142
|
+
bool_is_never_na: bool) -> str:
|
|
143
|
+
"""A field with no default, or `= na`, is an na of its type, `na(T)` (PyneComp):
|
|
144
|
+
`na(float)`, `na(bool)`, `na(Line)`, `na(list)` for an array. A field holding
|
|
145
|
+
its own class is made per instance, since the class is not defined yet."""
|
|
146
|
+
base = na_type.strip("'").split('[')[0]
|
|
147
|
+
if of_its_own_class:
|
|
148
|
+
return f'{FIELD_FACTORY}(default_factory=lambda: na({base}))'
|
|
149
|
+
return f'na({base})'
|