pine2pyne 0.2.4__py3-none-any.whl

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