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,604 @@
1
+ """
2
+ Pine semantics: the checks TradingView's compiler makes that the grammar does not (card #28).
3
+
4
+ One read-only pass over the parsed Script, run by transpile() and by
5
+ `--validate` between the parser and the transformer. It keeps a Pine scope
6
+ model - the global scope, one scope per function (its parameters and body) and
7
+ one per if/for/while/switch body - and collects diagnostics at a Pine line and
8
+ 1-based column: an identifier or call where one is at fault, else the
9
+ statement.
10
+
11
+ - A refusal is a program TradingView refuses whose transpile would give a
12
+ silent wrong value, drop a statement or fail at a Python line; enforce()
13
+ raises SemanticError for the first one and counts the rest.
14
+ - A warning is a program TradingView refuses that working files depend on
15
+ (operator decision 2026-09-24): a same-scope redeclaration, a read before the
16
+ declaration (from a function or block too) or in its own declaration.
17
+ enforce() reports each once per (line, column, name, code) as a
18
+ SemanticWarning, at its statement, when the script has no refusal; the
19
+ output is unchanged. Also a warning, though TradingView compiles it: a
20
+ `ta.*` call that runs on only some bars, whose history differs from the
21
+ every-bar call (conditional_history_calls.py, card #96). Never hoisted.
22
+
23
+ The pass never rewrites the tree, so a script it accepts transpiles exactly as
24
+ before. An assignment to a history reference (`x[1] := v`) is refused by the
25
+ parser instead: the tree keeps no trace of it.
26
+
27
+ This scope model is a second one beside scope_renamer's. scope_decisions()
28
+ exposes how it resolves each read, and a corpus test compares that with the
29
+ renamer's resolution, so a drift between the two fails.
30
+ """
31
+ import dataclasses
32
+ import itertools
33
+ import typing
34
+ import warnings
35
+ from typing import Dict, List, NamedTuple, Optional, Tuple
36
+
37
+ from .ast_nodes import (
38
+ Assignment, EnumDecl, Expression, ExpressionStatement, ForInLoop, ForLoop, FuncDecl,
39
+ FunctionCall, Identifier, IfExpression, IfStatement, IndexAccess, InputDecl, Literal, MemberAccess,
40
+ MethodCall, Reassignment, ReturnStatement, Script, Statement, SwitchExpression,
41
+ SwitchStatement, TupleDestructure, TypeDecl, VarDecl, VaripDecl, WhileLoop,
42
+ )
43
+ from .ast_walk import walk
44
+ from .conditional_history_calls import conditional_history_calls
45
+ from .errors import SemanticError, SemanticWarning
46
+ from .library_collision import (
47
+ FieldReadRule, HeadBinding, declared_type, global_binding_types, script_type_evidence,
48
+ )
49
+ from .namespace_members import NAMESPACE_MEMBERS
50
+ from .pine_builtins import PYNECORE_LIB_MODULES, SERIES_VARIABLES, input_positional_param_names
51
+ from .symbol_table import SymbolTable
52
+ from .udt_field_defaults import constant_heads, default_message, invalid_defaults, positioned_part
53
+
54
+ REFUSE, WARN = 'refuse', 'warn'
55
+ # Before Pine v3 a declaration may read its own name (v2 `x = nz(x[1]) + 1`)
56
+ LAST_SELF_REFERENCE_VERSION = 2
57
+ DISCARD = '_'
58
+ # `_` discards a value from Pine v5 on; before v5 reading it is UNPROVEN, so it is not refused
59
+ DISCARD_FROM_VERSION = 5
60
+ # Pine v6 removed the history of a field of a user-defined type, `t.x[1]` (migration guide to v6, "History of
61
+ # UDT fields"); v5 accepts it
62
+ FIELD_HISTORY_REFUSED_FROM_VERSION = 6
63
+ # TradingView's Pine v6 compiler refuses a history read of a `for i = a to b` counter: MEASURED 2026-10-06
64
+ # (VN301! 5m): `Error at 5:31: Variable "i" doesn't have history values (CE10078)`, 5:31 being the counter in
65
+ # `nz(i[1], -1)` (card #99, item 1). Earlier versions and the variables of `for ... in` are not measured
66
+ LOOP_COUNTER_HISTORY_REFUSED_FROM_VERSION = 6
67
+ # Pine v6's namespaces: the heads of the dotted names of the TradingView v6
68
+ # reference (pinescript-docs-scraper pine_reference_types.json, 2026-03-12)
69
+ PINE_NAMESPACES = frozenset({
70
+ 'adjustment', 'alert', 'array', 'backadjustment', 'barmerge', 'barstate', 'box', 'chart', 'color',
71
+ 'currency', 'dayofweek', 'display', 'dividends', 'earnings', 'extend', 'font', 'footprint', 'format',
72
+ 'hline', 'input', 'label', 'line', 'linefill', 'location', 'log', 'map', 'math', 'matrix', 'order',
73
+ 'plot', 'polyline', 'position', 'request', 'runtime', 'scale', 'session', 'settlement_as_close',
74
+ 'shape', 'size', 'splits', 'str', 'strategy', 'syminfo', 'ta', 'table', 'text', 'ticker',
75
+ 'timeframe', 'volume_row', 'xloc', 'yloc',
76
+ })
77
+ # Pine v6's bare built-in variables (the same reference)
78
+ PINE_BUILTIN_VARIABLES = frozenset({
79
+ 'ask', 'bar_index', 'bid', 'close', 'dayofmonth', 'dayofweek', 'false', 'high', 'hl2', 'hlc3',
80
+ 'hlcc4', 'hour', 'last_bar_index', 'last_bar_time', 'low', 'minute', 'month', 'na', 'ohlc4', 'open',
81
+ 'second', 'time', 'time_close', 'time_tradingday', 'timenow', 'true', 'volume', 'weekofyear', 'year',
82
+ })
83
+ # The bare built-in variables of Pine v4 and before that v5 moved into namespaces
84
+ LEGACY_BUILTIN_VARIABLES = frozenset({
85
+ 'accdist', 'iii', 'interval', 'isdaily', 'isdwm', 'isintraday', 'ismonthly', 'isweekly', 'n', 'nii',
86
+ 'nvi', 'obv', 'period', 'pvi', 'pvt', 'ticker', 'tickerid', 'tr', 'vwap', 'wad', 'wvad',
87
+ })
88
+ LAST_LEGACY_VERSION = 4
89
+ # Pine v3 and before: colours, styles, locations, shapes, sizes and days as bare constants
90
+ PRE_V4_CONSTANTS = frozenset({
91
+ 'aqua', 'black', 'blue', 'fuchsia', 'gray', 'green', 'lime', 'maroon', 'navy', 'olive', 'orange',
92
+ 'purple', 'red', 'silver', 'teal', 'white', 'yellow', 'line', 'stepline', 'histogram', 'cross', 'area',
93
+ 'columns', 'circles', 'solid', 'dotted', 'dashed', 'abovebar', 'belowbar', 'top', 'bottom', 'absolute',
94
+ 'xcross', 'circle', 'triangleup', 'triangledown', 'flag', 'arrowup', 'arrowdown', 'square', 'diamond',
95
+ 'labelup', 'labeldown', 'auto', 'tiny', 'small', 'normal', 'large', 'huge', 'monday', 'tuesday',
96
+ 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
97
+ })
98
+ LAST_PRE_V4_VERSION = 3
99
+ # An enum cannot take a namespace's name (`str` and `input` included, docs 7144);
100
+ # a UDT can (docs 6876: "UDT names shadow the language's namespaces")
101
+ RESERVED_ENUM_NAMES = PINE_NAMESPACES
102
+ # Names a bare read may mean without a declaration in the script: never out of scope
103
+ BUILTIN_NAMES = (PINE_BUILTIN_VARIABLES | PINE_NAMESPACES | frozenset(SymbolTable().builtin_symbols)
104
+ | SERIES_VARIABLES | PYNECORE_LIB_MODULES | frozenset(NAMESPACE_MEMBERS))
105
+ PARAMETER, LOOP_VARIABLE, INPUT, VARIABLE = 'parameter', 'loop variable', 'input', 'variable'
106
+ LOOP_COUNTER = 'loop counter' # the variable of `for i = a to b`: a loop variable that has no history
107
+ STATEMENT_TYPES = typing.get_args(Statement)
108
+ SCOPE_KIND_TEXT = {'if': 'an if block', 'for': 'a for loop', 'while': 'a while loop',
109
+ 'switch': 'a switch', 'function': 'a function'}
110
+ UNDECLARED_MESSAGE = ("'{}' is declared only inside a block or function that has ended here; "
111
+ "TradingView reports it as undeclared")
112
+
113
+
114
+ class Diagnostic(NamedTuple):
115
+ severity: str
116
+ code: str
117
+ line: int
118
+ column: int
119
+ name: str
120
+ message: str
121
+
122
+
123
+ class ReadDecision(NamedTuple):
124
+ """How the pass resolved one read: `binding` is (scope number, name), or
125
+ None when no declaration in the script is visible; `shadows` says whether
126
+ that binding hides one of the same name in an enclosing scope (a global
127
+ counts wherever it is declared, as scope_renamer binds every global first);
128
+ `later_global` marks an unresolved read of a global declared further down."""
129
+ binding: Optional[Tuple[int, str]]
130
+ shadows: bool
131
+ later_global: bool
132
+
133
+
134
+ @dataclasses.dataclass
135
+ class _Scope:
136
+ number: int
137
+ kind: str
138
+ names: Dict[str, str] = dataclasses.field(default_factory=dict) # name -> how it was declared
139
+ types: Dict[str, Optional[str]] = dataclasses.field(default_factory=dict) # name -> its declared Pine type
140
+ shadowing: set = dataclasses.field(default_factory=set) # names declared here that hide an outer one
141
+ declared_ahead: frozenset = frozenset() # names the scope's own statements declare, walked or not
142
+ ended: set = dataclasses.field(default_factory=set) # names bound in blocks nested here that have ended
143
+
144
+
145
+ def check_script(script: Script) -> List[Diagnostic]:
146
+ """Every diagnostic for `script`, in source order."""
147
+ return _SemanticCheck(script).run()
148
+
149
+
150
+ def enforce(script: Script) -> None:
151
+ """Raise SemanticError for the first refusal, naming how many more there
152
+ are; a script with no refusal gets one SemanticWarning per (line, column,
153
+ name, code). A refused script is not warned about: the refusal is its
154
+ verdict, also under `-W error`."""
155
+ diagnostics = check_script(script)
156
+ refusals = [diagnostic for diagnostic in diagnostics if diagnostic.severity == REFUSE]
157
+ if refusals:
158
+ first = refusals[0]
159
+ raise SemanticError(first.code, first.message, first.line, first.column, len(refusals) - 1)
160
+ warned = {(diagnostic.line, diagnostic.column, diagnostic.name, diagnostic.code): diagnostic
161
+ for diagnostic in diagnostics if diagnostic.severity == WARN}
162
+ for diagnostic in warned.values():
163
+ warnings.warn(SemanticWarning(diagnostic.code, diagnostic.message, diagnostic.line, diagnostic.column),
164
+ stacklevel=3)
165
+
166
+
167
+ def scope_decisions(script: Script) -> Dict[Tuple[int, int, str], ReadDecision]:
168
+ """(line, column, name) of every positioned bare-name read -> how the pass resolved it."""
169
+ check = _SemanticCheck(script)
170
+ check.run()
171
+ return check.decisions
172
+
173
+
174
+ def _declared_names(node) -> List[str]:
175
+ if isinstance(node, (VarDecl, VaripDecl, InputDecl)):
176
+ return [node.name]
177
+ if isinstance(node, Assignment):
178
+ return list(node.target.names) if isinstance(node.target, TupleDestructure) else [node.target]
179
+ return []
180
+
181
+
182
+ def _pine_version(script: Script) -> int:
183
+ version = script.version.version if script.version else ''
184
+ return int(version) if version.isdigit() else 1 # no annotation: Pine v1
185
+
186
+
187
+ class _SemanticCheck:
188
+ def __init__(self, script: Script):
189
+ self.script = script
190
+ self.version = _pine_version(script)
191
+ self.constant_heads = constant_heads(script)
192
+ self.builtin_names = BUILTIN_NAMES | (LEGACY_BUILTIN_VARIABLES if self.version <= LAST_LEGACY_VERSION
193
+ else frozenset()) \
194
+ | (PRE_V4_CONSTANTS if self.version <= LAST_PRE_V4_VERSION else frozenset())
195
+ self.top_items = sorted(list(script.declarations) + list(script.body), key=lambda item: item.line)
196
+ self.diagnostics: List[Diagnostic] = []
197
+ self.decisions: Dict[Tuple[int, int, str], ReadDecision] = {}
198
+ self.line, self.column = 0, 0
199
+ self.declaring: frozenset = frozenset() # the names the statement being walked declares
200
+ self.scope_numbers = itertools.count()
201
+ self.global_names = {name for item in self.top_items for name in _declared_names(item)}
202
+ self.field_read_rule = FieldReadRule(script)
203
+ self.global_types = global_binding_types(script)
204
+ self.type_evidence = script_type_evidence(script)
205
+ every_node = list(walk(self.top_items))
206
+ self.local_only = _names_declared_in(every_node) - self.global_names
207
+ self.function_names = {node.name for node in every_node if isinstance(node, FuncDecl)}
208
+ self.type_names = {node.name for node in every_node if isinstance(node, (TypeDecl, EnumDecl))}
209
+ declared_count: Dict[str, int] = {}
210
+ for item in self.top_items:
211
+ if isinstance(item, FuncDecl) and not item.is_method:
212
+ declared_count[item.name] = declared_count.get(item.name, 0) + 1
213
+ self.overloaded = {name for name, count in declared_count.items() if count > 1}
214
+
215
+ # ------------------------------------------------------------ reporting
216
+
217
+ def report(self, severity: str, code: str, name: str, message: str, node=None) -> None:
218
+ """A diagnostic at `node`'s own position when it has one, else the statement's."""
219
+ line, column = (node.line, node.column) if getattr(node, 'line', 0) else (self.line, self.column)
220
+ self.diagnostics.append(Diagnostic(severity, code, line, column, name, message))
221
+
222
+ def at(self, node) -> None:
223
+ if getattr(node, 'line', 0):
224
+ self.line, self.column = node.line, node.column
225
+
226
+ def at_expression(self, node) -> None:
227
+ """An else-if condition or a switch case comes after a block that moved the
228
+ position: take its own, from its first positioned part."""
229
+ self.at(next((part for part in walk(node) if getattr(part, 'line', 0)), None))
230
+
231
+ # ------------------------------------------------------------ scopes
232
+
233
+ def new_scope(self, kind: str, statements=()) -> _Scope:
234
+ declared = [name for statement in statements for name in _declared_names(statement)] \
235
+ if isinstance(statements, list) else []
236
+ return _Scope(next(self.scope_numbers), kind, declared_ahead=frozenset(declared))
237
+
238
+ @staticmethod
239
+ def declared_later(stack: List[_Scope], name: str) -> bool:
240
+ """`name` is declared further down a scope that is still open (it is not bound yet)."""
241
+ return any(name in scope.declared_ahead for scope in stack)
242
+
243
+ def out_of_scope(self, stack: List[_Scope], name: str) -> bool:
244
+ """An unbound `name` that only a scope which has ended declared so far: bound in
245
+ an ended if/for/while/switch block of the reader's own scopes (the emitted
246
+ Python would read that block's stale value; a name the statement itself
247
+ declares is a self-reference instead), or local to other scopes and not
248
+ declared later in an open one."""
249
+ return ((name not in self.declaring and any(name in scope.ended for scope in self.own_scopes(stack)))
250
+ or (name in self.local_only and not self.declared_later(stack, name)))
251
+
252
+ @staticmethod
253
+ def own_scopes(stack: List[_Scope]) -> List[_Scope]:
254
+ """The open scopes a read sees as its own: from the innermost function
255
+ scope in (a function reading a global is its own case), else all."""
256
+ functions = [index for index, scope in enumerate(stack) if scope.kind == 'function']
257
+ return stack[functions[-1]:] if functions else stack
258
+
259
+ def declare(self, stack: List[_Scope], name: str, how: str, type_name: Optional[str] = None) -> None:
260
+ if name == DISCARD:
261
+ return
262
+ scope = stack[-1]
263
+ earlier = scope.names.get(name)
264
+ if earlier is None:
265
+ outer = any(name in enclosing.names for enclosing in stack[:-1])
266
+ if outer or (len(stack) > 1 and name in self.global_names):
267
+ scope.shadowing.add(name)
268
+ elif INPUT in (earlier, how): # an input is declared only at global scope (parser.py top-level loop)
269
+ self.report(REFUSE, 'duplicate-input', name, f"'{name}' is already declared in this scope "
270
+ f'(an input and another declaration cannot share a name)')
271
+ elif earlier not in (LOOP_VARIABLE, LOOP_COUNTER): # a parameter too: UNPROVEN on TradingView, so a warning
272
+ self.report(WARN, 'redeclared', name, f"'{name}' is already declared in this scope; "
273
+ f'TradingView refuses a redeclaration, the later one is used here')
274
+ scope.names.setdefault(name, how)
275
+ scope.types.setdefault(name, type_name) # the first declaration's, as scope_renamer binds it
276
+
277
+ @staticmethod
278
+ def depth_of(stack: List[_Scope], name: str) -> Optional[int]:
279
+ for depth in range(len(stack) - 1, -1, -1):
280
+ if name in stack[depth].names:
281
+ return depth
282
+ return None
283
+
284
+ def read(self, stack: List[_Scope], identifier: Identifier) -> None:
285
+ name = identifier.name
286
+ depth = self.depth_of(stack, name)
287
+ if identifier.line:
288
+ binding = (stack[depth].number, name) if depth is not None else None
289
+ shadows = depth is not None and name in stack[depth].shadowing
290
+ later_global = depth is None and name in self.global_names
291
+ self.decisions[identifier.line, identifier.column, name] = ReadDecision(binding, shadows, later_global)
292
+ if name == DISCARD:
293
+ if self.version >= DISCARD_FROM_VERSION:
294
+ self.report(REFUSE, 'discard-read', name, "'_' discards a value and cannot be read", identifier)
295
+ return
296
+ if depth is not None or name in self.builtin_names or name in self.function_names or name in self.type_names:
297
+ return
298
+ if self.out_of_scope(stack, name):
299
+ self.report(REFUSE, 'out-of-scope', name, UNDECLARED_MESSAGE.format(name), identifier)
300
+ elif name in self.global_names or self.declared_later(stack, name):
301
+ self.read_before_declaration(name)
302
+
303
+ def read_before_declaration(self, name: str) -> None:
304
+ """A read above the name's declaration in a scope that is still open, from
305
+ any inner scope (a function reading a later global is UNPROVEN on
306
+ TradingView), or in its own declaration."""
307
+ if name not in self.declaring:
308
+ self.report(WARN, 'used-before-declaration', name, f"'{name}' is read before its declaration")
309
+ elif self.version > LAST_SELF_REFERENCE_VERSION:
310
+ self.report(WARN, 'self-reference', name, f"'{name}' is read in its own declaration")
311
+
312
+ # ------------------------------------------------------------ statements
313
+
314
+ def run(self) -> List[Diagnostic]:
315
+ self.recursion()
316
+ stack = [self.new_scope('global')]
317
+ for item in self.top_items:
318
+ self.statement(item, stack)
319
+ self.conditional_history_calls()
320
+ return sorted(self.diagnostics, key=lambda diagnostic: (diagnostic.line, diagnostic.column))
321
+
322
+ def conditional_history_calls(self) -> None:
323
+ """A `ta.*` call that runs on only some bars keeps a history that differs from the
324
+ every-bar call (card #96). A warning only: the call stays where the script has it."""
325
+ for call in conditional_history_calls(self.script):
326
+ self.report(WARN, 'conditional-history-call', call.func,
327
+ f"'{call.func}' is called only when its condition holds: its series history "
328
+ f'differs from an unconditional call (the output keeps the call where it is, as TradingView '
329
+ f'does; TradingView advises calling it on every bar, in the global scope)', call)
330
+
331
+ def block(self, statements, stack: List[_Scope], kind: str, names=(), how=LOOP_VARIABLE,
332
+ types: Optional[Dict[str, Optional[str]]] = None) -> None:
333
+ inner = stack + [self.new_scope(kind, statements if statements is not None else ())]
334
+ for name in names:
335
+ self.declare(inner, name, how, (types or {}).get(name))
336
+ if isinstance(statements, list):
337
+ for statement in statements:
338
+ self.statement(statement, inner)
339
+ elif statements is not None:
340
+ self.expression(statements, inner)
341
+ if kind != 'function': # a Python def keeps its locals; an if/for block leaks them
342
+ stack[-1].ended.update(inner[-1].names, inner[-1].ended)
343
+
344
+ def statement(self, node, stack: List[_Scope]) -> None:
345
+ self.at(node)
346
+ if isinstance(node, FuncDecl):
347
+ self.function(node, stack)
348
+ elif isinstance(node, (VarDecl, VaripDecl, Assignment)):
349
+ enclosing = self.declaring
350
+ self.declaring = enclosing | frozenset(_declared_names(node))
351
+ self.expression(node.value, stack)
352
+ self.declaring = enclosing
353
+ self.at(node) # a block in the value moved the position
354
+ for name in _declared_names(node):
355
+ self.declare(stack, name, VARIABLE, declared_type(node, self.type_evidence._replace(
356
+ variable_type=lambda variable: self.variable_type(variable, stack))))
357
+ elif isinstance(node, InputDecl):
358
+ self.expression([node.args, list(node.kwargs.values())], stack)
359
+ self.input_arguments(node, stack)
360
+ self.declare(stack, node.name, INPUT, declared_type(node))
361
+ elif isinstance(node, EnumDecl) and node.name in RESERVED_ENUM_NAMES:
362
+ self.report(REFUSE, 'named-like-namespace', node.name,
363
+ f"an enum cannot be named '{node.name}', the name of a built-in namespace")
364
+ elif isinstance(node, TypeDecl):
365
+ for field_name, default in invalid_defaults(node, self.constant_heads):
366
+ self.report(REFUSE, 'udt-field-default', field_name, default_message(node, field_name),
367
+ positioned_part(default))
368
+ elif isinstance(node, Reassignment):
369
+ self.expression(node.value, stack)
370
+ self.at(node) # a block in the value moved the position
371
+ self.reassignment(node.target, stack)
372
+ elif isinstance(node, (IfStatement, ForLoop, ForInLoop, WhileLoop)):
373
+ self.control_flow(node, stack)
374
+ elif isinstance(node, SwitchStatement):
375
+ self.switch(node, stack)
376
+ elif isinstance(node, (ExpressionStatement, ReturnStatement)):
377
+ self.expression(node.expr, stack)
378
+ elif isinstance(node, Expression):
379
+ self.expression(node, stack)
380
+
381
+ def input_arguments(self, node: InputDecl, stack: List[_Scope]) -> None:
382
+ """An input's arguments must be const, so they cannot read another input (manual,
383
+ Inputs: "results from other input*() calls"); `active` takes an input bool, and
384
+ input.source's `defval` a series float (card #89)."""
385
+ parameters = ['defval', 'title'] + input_positional_param_names(node.func, node.args)
386
+ # a row that does not name `active` ends at `display`: a positional argument after it is `active`
387
+ positional = (parameters[:parameters.index('display') + 1]
388
+ if 'display' in parameters and 'active' not in parameters else parameters)
389
+ arguments = list(zip(positional, node.args)) + list(node.kwargs.items())
390
+ for parameter, argument in arguments:
391
+ if parameter == 'active' or (parameter == 'defval' and node.func == 'input.source'):
392
+ continue
393
+ for read in walk(argument):
394
+ if isinstance(read, Identifier) and self.depth_of(stack, read.name) == 0 \
395
+ and stack[0].names[read.name] == INPUT:
396
+ self.report(REFUSE, 'input-argument-reads-input', read.name,
397
+ f"the '{parameter}' argument of input '{node.name}' reads the input "
398
+ f"'{read.name}'; TradingView requires a const argument there", read)
399
+
400
+ def control_flow(self, node, stack: List[_Scope]) -> None:
401
+ if isinstance(node, IfStatement):
402
+ self.expression(node.condition, stack)
403
+ self.block(node.body, stack, 'if')
404
+ for condition, body in node.elseifs:
405
+ self.at_expression(condition)
406
+ self.expression(condition, stack)
407
+ self.block(body, stack, 'if')
408
+ self.block(node.else_body, stack, 'if')
409
+ elif isinstance(node, ForLoop):
410
+ self.expression([node.from_val, node.to_val, node.step], stack)
411
+ self.block(node.body, stack, 'for', [node.var], LOOP_COUNTER)
412
+ elif isinstance(node, ForInLoop):
413
+ self.expression(node.iterable, stack)
414
+ self.block(node.body, stack, 'for', node.vars)
415
+ else:
416
+ self.expression(node.condition, stack)
417
+ self.block(node.body, stack, 'while')
418
+
419
+ def function(self, node: FuncDecl, stack: List[_Scope]) -> None:
420
+ if len(stack) > 1:
421
+ self.report(REFUSE, 'nested-function', node.name, f"function '{node.name}' is declared inside "
422
+ f'{SCOPE_KIND_TEXT[stack[-1].kind]}; TradingView allows a function declaration only '
423
+ f'at global scope')
424
+ self.expression([parameter.default for parameter in node.params], stack)
425
+ self.block(node.body, stack, 'function', [parameter.name for parameter in node.params], PARAMETER,
426
+ {parameter.name: declared_type(parameter) for parameter in node.params})
427
+
428
+ def reassignment(self, target: str, stack: List[_Scope]) -> None:
429
+ if '.' in target: # a field: `p.v := x` is valid anywhere
430
+ return
431
+ depth = self.depth_of(stack, target)
432
+ functions = [index for index, scope in enumerate(stack) if scope.kind == 'function']
433
+ if depth is None:
434
+ if self.out_of_scope(stack, target):
435
+ self.report(REFUSE, 'out-of-scope', target, UNDECLARED_MESSAGE.format(target))
436
+ elif functions and target in self.global_names:
437
+ self.report(REFUSE, 'global-modified-in-function', target,
438
+ f"a function cannot modify the global variable '{target}'")
439
+ else:
440
+ self.report(REFUSE, 'reassigned-undeclared', target, f"'{target}' is not declared before "
441
+ f"this ':='; TradingView refuses a reassignment of an undeclared variable")
442
+ elif functions and depth < functions[-1]:
443
+ self.report(REFUSE, 'global-modified-in-function', target,
444
+ f"a function cannot modify the {'global' if depth == 0 else 'enclosing'} variable '{target}'")
445
+ elif stack[depth].names.get(target) == PARAMETER:
446
+ self.report(REFUSE, 'parameter-modified', target, f"a function cannot modify its parameter '{target}'")
447
+
448
+ def switch(self, node, stack: List[_Scope]) -> None:
449
+ self.expression(node.expr, stack)
450
+ for case, body in node.cases:
451
+ self.at_expression(case)
452
+ self.expression(case, stack)
453
+ self.block(body, stack, 'switch')
454
+ self.block(node.default, stack, 'switch')
455
+
456
+ # ------------------------------------------------------------ expressions
457
+
458
+ def expression(self, node, stack: List[_Scope]) -> None:
459
+ if node is None or isinstance(node, (str, int, float, bool)):
460
+ return
461
+ if isinstance(node, (list, tuple)):
462
+ for item in node:
463
+ self.expression(item, stack)
464
+ elif isinstance(node, dict):
465
+ self.expression(list(node.values()), stack)
466
+ elif isinstance(node, Identifier):
467
+ if '.' not in node.name: # `N.m` names a namespace, a type or a library unless N is in scope
468
+ self.read(stack, node)
469
+ elif isinstance(node, IndexAccess):
470
+ if isinstance(node.object, IndexAccess):
471
+ self.report(REFUSE, 'double-history', '', 'the [] history operator can be used only once on '
472
+ 'the same value (close[1][2])', node)
473
+ elif self.version >= FIELD_HISTORY_REFUSED_FROM_VERSION and self.field_read_rule.is_field_read(
474
+ node.object, lambda name: self.binding_in_scope(name, stack)):
475
+ self.report(REFUSE, 'history-on-udt-field', '', self.field_history_message(node), node)
476
+ elif self.version >= LOOP_COUNTER_HISTORY_REFUSED_FROM_VERSION and self.is_loop_counter(node.object, stack):
477
+ self.report(REFUSE, 'loop-counter-history', node.object.name,
478
+ f'Variable "{node.object.name}" doesn\'t have history values', node.object)
479
+ self.expression([node.object, node.index], stack)
480
+ elif isinstance(node, FunctionCall):
481
+ if not isinstance(node.func, str):
482
+ self.expression(node.func, stack)
483
+ self.expression([node.args, list(node.kwargs.values())], stack)
484
+ elif isinstance(node, MethodCall):
485
+ self.expression([node.object, node.args, list(node.kwargs.values())], stack)
486
+ elif isinstance(node, MemberAccess):
487
+ if not isinstance(node.object, str):
488
+ self.expression(node.object, stack)
489
+ elif isinstance(node, IfExpression):
490
+ self.expression(node.condition, stack)
491
+ self.block(node.true_expr, stack, 'if')
492
+ self.block(node.false_expr, stack, 'if')
493
+ elif isinstance(node, SwitchExpression):
494
+ self.switch(node, stack)
495
+ elif isinstance(node, STATEMENT_TYPES):
496
+ self.statement(node, stack)
497
+ elif dataclasses.is_dataclass(node):
498
+ self.expression([getattr(node, field.name) for field in dataclasses.fields(node)
499
+ if field.name not in ('line', 'column')], stack)
500
+
501
+ def is_loop_counter(self, node, stack: List[_Scope]) -> bool:
502
+ """`node` reads the counter of a `for i = a to b` loop that is open here: the innermost binding of
503
+ its name is the counter, so a name a nested block declares again is that declaration's, not the counter's.
504
+ A name the loop body declares again at the counter's own level keeps the counter's binding (a scope's
505
+ first declaration wins) and is refused: UNMEASURED on TradingView, which refuses the redeclaration itself."""
506
+ depth = self.depth_of(stack, node.name) if isinstance(node, Identifier) else None
507
+ return depth is not None and stack[depth].names[node.name] == LOOP_COUNTER
508
+
509
+ def binding_in_scope(self, name: str, stack: List[_Scope]) -> Optional[HeadBinding]:
510
+ """The variable `name` names here, with its declared type: the binding in scope, or a
511
+ global (declared above or further down, typed as the scope renamer types it, by its
512
+ first global declaration); None when `name` is no variable here (card #88)."""
513
+ depth = self.depth_of(stack, name)
514
+ if depth is not None and depth > 0: # a global (depth 0) is typed below, in the renamer's order
515
+ return HeadBinding(stack[depth].types.get(name))
516
+ return HeadBinding(self.global_types.get(name)) if name in self.global_names else None
517
+
518
+ def variable_type(self, name: str, stack: List[_Scope]) -> Optional[str]:
519
+ """The type of the variable `name` names here (`binding_in_scope`), None when unknown."""
520
+ binding = self.binding_in_scope(name, stack)
521
+ return binding.type_name if binding else None
522
+
523
+ @staticmethod
524
+ def field_history_message(site: IndexAccess) -> str:
525
+ """The advice indexes the object, then walks the field path: `o.i.v[1]` -> `(o[1]).i.v`
526
+ (card #88; `(o.i[1]).v` would index a field again)."""
527
+ index = site.index.value if isinstance(site.index, Literal) else 'n'
528
+ spelled = site.object.name if isinstance(site.object, Identifier) else ''
529
+ if isinstance(site.object, MemberAccess) and isinstance(site.object.object, str):
530
+ spelled = f'{site.object.object}.{site.object.member}'
531
+ head, _, path = spelled.partition('.')
532
+ object_text, field_text = (head, path) if path else ('object', 'field')
533
+ return (f"the [] history operator cannot be used on a field of a user-defined type from Pine v6; "
534
+ f"write ({object_text}[{index}]).{field_text} for the field of the object {index} bar(s) back, "
535
+ f"or assign the field to a variable and index that variable")
536
+
537
+ # ------------------------------------------------------------ recursion
538
+
539
+ def recursion(self) -> None:
540
+ """A call that closes a cycle in the call graph of the script's functions,
541
+ refused at the call. An overloaded name resolves by signature, so it is
542
+ left out of the graph (expds_097 calls one overload from another); an
543
+ overload that passes its own parameters, unchanged, to its own name
544
+ resolves to itself and is refused."""
545
+ declared = [item for item in self.top_items if isinstance(item, FuncDecl) and not item.is_method]
546
+ # a call by a method's name may select the method (`f(P.new(x))` beside `method f(P this)`)
547
+ method_names = {item.name for item in self.top_items if isinstance(item, FuncDecl) and item.is_method}
548
+ single = {function.name: function for function in declared
549
+ if function.name not in self.overloaded and function.name not in method_names}
550
+ calls = {name: [node for node in walk(function.body)
551
+ if isinstance(node, FunctionCall) and isinstance(node.func, str) and node.func in single]
552
+ for name, function in single.items()}
553
+ callees = {name: {call.func for call in found} for name, found in calls.items()}
554
+ for name, function in single.items():
555
+ closing = next((call for call in calls[name]
556
+ if call.func == name or self.reaches(call.func, name, callees)), None)
557
+ if closing is not None:
558
+ self.report_recursion(name, closing)
559
+ for function in declared:
560
+ if function.name in self.overloaded:
561
+ own = next((node for node in walk(function.body) if _passes_own_parameters(node, function)), None)
562
+ if own is not None:
563
+ self.report_recursion(function.name, own)
564
+
565
+ def report_recursion(self, name: str, call: FunctionCall) -> None:
566
+ self.report(REFUSE, 'recursion', name, f"function '{name}' calls itself (directly or through another "
567
+ f'function); TradingView does not allow recursion', call)
568
+
569
+ @staticmethod
570
+ def reaches(start: str, goal: str, callees: Dict[str, set]) -> bool:
571
+ seen, pending = set(), list(callees[start])
572
+ while pending:
573
+ name = pending.pop()
574
+ if name == goal:
575
+ return True
576
+ if name not in seen:
577
+ seen.add(name)
578
+ pending.extend(callees.get(name, ()))
579
+ return False
580
+
581
+
582
+ def _names_declared_in(nodes: list) -> set:
583
+ """Every name a declaration, loop or parameter in `nodes` binds."""
584
+ names = {name for node in nodes for name in _declared_names(node)}
585
+ for node in nodes:
586
+ if isinstance(node, ForLoop):
587
+ names.add(node.var)
588
+ elif isinstance(node, ForInLoop):
589
+ names.update(node.vars)
590
+ elif isinstance(node, FuncDecl):
591
+ names.update(parameter.name for parameter in node.params)
592
+ return names
593
+
594
+
595
+ def _passes_own_parameters(node, function: FuncDecl) -> bool:
596
+ """`f(x)` inside `f(float x)`: the call's arguments are the function's own
597
+ parameters, in order and unchanged (no local of their names), so it
598
+ selects this very overload."""
599
+ if not (isinstance(node, FunctionCall) and node.func == function.name and not node.kwargs):
600
+ return False
601
+ parameters = [parameter.name for parameter in function.params]
602
+ arguments = [argument.name if isinstance(argument, Identifier) else None for argument in node.args]
603
+ redeclared = _names_declared_in(list(walk(function.body)))
604
+ return arguments == parameters and not redeclared.intersection(parameters)
@@ -0,0 +1,18 @@
1
+ """Application layer: the transpile use case and the stages it is built from."""
2
+ from .service import TranspileService
3
+ from .stages import (
4
+ CommentOnlySourceFilter,
5
+ FreshTransformer,
6
+ LegacyFunctionNameNormalizer,
7
+ PineSemanticGuard,
8
+ RepairingSourceParser,
9
+ )
10
+
11
+ __all__ = [
12
+ 'CommentOnlySourceFilter',
13
+ 'FreshTransformer',
14
+ 'LegacyFunctionNameNormalizer',
15
+ 'PineSemanticGuard',
16
+ 'RepairingSourceParser',
17
+ 'TranspileService',
18
+ ]