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,694 @@
1
+ """
2
+ Block-value lowering: an `if`/`switch` EXPRESSION whose branch is a block, and
3
+ a loop used as a value.
4
+
5
+ In Pine, a branch of an if/switch used as a value may hold several statements;
6
+ they run when the branch is taken, and its last expression is the value:
7
+
8
+ y = if c
9
+ z = close
10
+ z * 2
11
+ else
12
+ 0.0
13
+
14
+ A Python conditional expression cannot hold statements (card #41: the branch
15
+ was emitted as `None`). This pre-pass rewrites every such value into the idiom
16
+ Pine authors write by hand, which the transformer already emits correctly:
17
+
18
+ y = na (keeps a declared type; untyped is Pine's typeless na)
19
+ if c
20
+ z = close
21
+ y := z * 2
22
+ else
23
+ y := 0.0
24
+
25
+ A block-valued switch becomes an if / else-if chain over a temporary that holds
26
+ the subject, so the subject is evaluated once. `var`/`varip` targets run the
27
+ lowered statements once, behind a flag. A branch that is a single binding
28
+ (`k := close`) is a block too: its binding runs, and its value is k (card #50).
29
+
30
+ A loop used as a value (`s = for …`, `s = while …`, a branch or a function tail
31
+ that ends in a loop) yields what its body's tail gave on the last iteration
32
+ that evaluated it, na when no iteration ran (Pine docs, Loops). Each value is
33
+ written to a fresh local, so the body still reads the target's old value:
34
+
35
+ s = for i = 0 to 2 s = na
36
+ i * 10 -> loop_value = na
37
+ for i = 0 to 2
38
+ loop_value = i * 10
39
+ s := loop_value
40
+
41
+ An arm that does not run (an if without else) yields false for a bool value
42
+ and na otherwise; a tuple-returning one yields a tuple of na as wide as its
43
+ tuples (cards #50, #54). `s op= <block>` reads s, then runs the block into a
44
+ fresh local (premise P1: Pine's operand order is UNVERIFIED on TradingView).
45
+ `[a, b] = <if/switch/loop>` fills a fresh local that starts as a tuple of na,
46
+ destructured once. A block value anywhere else (an operand of an operator, a
47
+ call argument) is refused with its line, never emitted.
48
+ """
49
+ import dataclasses
50
+ from typing import Callable, Dict, List, Optional
51
+
52
+ from .ast_nodes import (
53
+ ArrayLiteral, Assignment, BinaryOp, BreakStatement, ContinueStatement,
54
+ Expression, ExpressionStatement, ForInLoop, ForLoop, FuncDecl, FunctionCall,
55
+ Identifier, IfExpression, IfStatement, Literal, NaLiteral, Reassignment,
56
+ Script, SwitchExpression, SwitchStatement, TernaryOp, TupleDestructure, UnaryOp,
57
+ VarDecl, VaripDecl, WhileLoop,
58
+ )
59
+ from .ast_walk import child_field_names, contains, walk
60
+ from .errors import UnsupportedFeatureError
61
+ from .scope_renamer import collect_names, fresh_name
62
+
63
+ VALUE_EXPRESSIONS = (IfExpression, SwitchExpression)
64
+ LOOPS = (ForLoop, ForInLoop, WhileLoop)
65
+ BINDINGS = (Assignment, Reassignment, VarDecl, VaripDecl)
66
+ # The nodes whose direct child is a value: a loop there is a loop used as a value
67
+ VALUE_HOLDERS = (Expression,) + BINDINGS
68
+ # Half the nodes of a script; they hold no node, so the pre-check skips them
69
+ CHILDLESS_NODE_TYPES = frozenset({Identifier, Literal})
70
+ # A branch ending in one of these has no value of its own to extract
71
+ VALUELESS_TAILS = (IfStatement, SwitchStatement) + LOOPS
72
+ LOOP_HEADER_FIELDS = ('from_val', 'to_val', 'step', 'iterable', 'condition')
73
+ # The parser reads `s += v` as `s := s + v`
74
+ COMPOUND_OPERATORS = ('+', '-', '*', '/', '%')
75
+ BOOL_OPERATORS = ('==', '!=', '<', '<=', '>', '>=', 'and', 'or')
76
+
77
+ Write = Callable[[Expression], object] # the statement that takes a value
78
+
79
+
80
+ def lower_block_values(script: Script) -> Script:
81
+ """Return a copy of `script` with every block value lowered to statements.
82
+ A script without one is returned unchanged. An untaken bool arm is false
83
+ only where `stamp_bool_values` stamped its value first (card #56)."""
84
+ if not _needs_lowering_somewhere(script):
85
+ return script
86
+ return _BlockValueLowering(collect_names(script), _UserFunctions.of(script),
87
+ bool_is_never_na(script)).script(script)
88
+
89
+
90
+ def _needs_lowering(node) -> bool:
91
+ """A node other than a function that this pass rewrites or refuses: a block
92
+ value, a loop as a value or an operand, a tuple target of an if/switch."""
93
+ if isinstance(node, VALUE_EXPRESSIONS) and has_block_branch(node):
94
+ return True
95
+ if isinstance(node, VALUE_HOLDERS) and _has_loop_operand(node):
96
+ return True
97
+ return (isinstance(node, Assignment) and isinstance(node.target, TupleDestructure)
98
+ and _has_untaken_arm(node.value))
99
+
100
+
101
+ def _needs_lowering_somewhere(script: Script) -> bool:
102
+ """True when this pass would rewrite or refuse something in `script`: a
103
+ node `_needs_lowering` names, a function whose tail reaches a loop, or a
104
+ function whose tail if/switch has an arm that may not run and that yields
105
+ tuples or is destructured, the tail `tail_is_lowered` lowers or refuses.
106
+ Each function is checked on its own: overloads share a name, and the parser
107
+ accepts a function in a block. Other such functions are left to the
108
+ transformer, so they do not cost this pass a copy of the script. This runs
109
+ on every node of every script, hence the dispatch on the node's type
110
+ (card #50, transpile-time gate)."""
111
+ untaken_tails = []
112
+ for node in walk(script):
113
+ if type(node) in CHILDLESS_NODE_TYPES:
114
+ continue
115
+ if isinstance(node, FuncDecl):
116
+ if _tail_reaches_loop(node.body):
117
+ return True
118
+ tail = _untaken_tail(node)
119
+ if tail is not None:
120
+ untaken_tails.append((node.name, tail))
121
+ elif _needs_lowering(node):
122
+ return True
123
+ if not untaken_tails:
124
+ return False
125
+ functions = _UserFunctions.of(script)
126
+ return any(_tuple_width(tail, functions) or name in functions.destructured
127
+ for name, tail in untaken_tails)
128
+
129
+
130
+ def _has_loop_operand(node) -> bool:
131
+ """True when a loop is a direct child of `node`: a bound value, an operand.
132
+ Runs on every expression of every script, so it reads the fields once and
133
+ builds no lists (card #50, transpile-time gate)."""
134
+ for name in child_field_names(type(node)):
135
+ child = getattr(node, name)
136
+ if isinstance(child, LOOPS) or (type(child) is list and any(isinstance(item, LOOPS) for item in child)):
137
+ return True
138
+ return False
139
+
140
+
141
+ def _branches(expression) -> list:
142
+ if isinstance(expression, IfExpression):
143
+ return [expression.true_expr, expression.false_expr]
144
+ return [body for _, body in expression.cases] + [expression.default]
145
+
146
+
147
+ def _tail_expression(branch):
148
+ tail = branch[-1] if isinstance(branch, list) and branch else branch
149
+ return tail.expr if isinstance(tail, ExpressionStatement) else tail
150
+
151
+
152
+ def has_block_branch(expression) -> bool:
153
+ """True when a branch of this if/switch expression, or of one in a branch
154
+ tail, is a block: several statements, a sole binding, or a tail statement
155
+ with no value. A default-only switch with a subject counts too: its
156
+ subject runs as a statement before the default, so its side effect is
157
+ kept (card #43)."""
158
+ if not isinstance(expression, VALUE_EXPRESSIONS):
159
+ return False
160
+ if isinstance(expression, SwitchExpression) and not expression.cases and expression.expr is not None:
161
+ return True
162
+ for branch in _branches(expression):
163
+ if isinstance(branch, LOOPS):
164
+ return True # an inline switch case `0 => for …` keeps its loop bare
165
+ if isinstance(branch, list) and branch and (len(branch) > 1
166
+ or isinstance(branch[-1], VALUELESS_TAILS + BINDINGS)):
167
+ # a sole binding `k := close` is a block: the binding must run (card #50)
168
+ return True
169
+ tail = _tail_expression(branch)
170
+ if has_block_branch(tail):
171
+ return True
172
+ if isinstance(tail, BINDINGS) and has_block_branch(tail.value):
173
+ # `z = if <block> …` as the branch: z's statements run, z is the value
174
+ return True
175
+ return False
176
+
177
+
178
+ def _is_block_value(node) -> bool:
179
+ """A value that only statements can compute: a loop, or a block-valued if/switch."""
180
+ return isinstance(node, LOOPS) or (isinstance(node, VALUE_EXPRESSIONS) and has_block_branch(node))
181
+
182
+
183
+ def _contains_block_value(value) -> bool:
184
+ return contains(value, _is_block_value)
185
+
186
+
187
+ def _target_name(statement) -> Optional[str]:
188
+ """The variable a declaration or assignment binds, when it is a plain name."""
189
+ name = statement.name if isinstance(statement, (VarDecl, VaripDecl)) else getattr(statement, 'target', None)
190
+ return name if isinstance(name, str) else None
191
+
192
+
193
+ def _is_compound_block(node) -> bool:
194
+ """`s op= <block value>`, read by the parser as `s := s op <block value>`."""
195
+ value = getattr(node, 'value', None)
196
+ return (isinstance(node, Reassignment) and isinstance(value, BinaryOp) and value.op in COMPOUND_OPERATORS
197
+ and isinstance(value.left, Identifier) and value.left.name == node.target
198
+ and _is_block_value(value.right))
199
+
200
+
201
+ def _untaken_tail(node: FuncDecl):
202
+ """The tail if/switch of a function body when an arm of it may not run, else None."""
203
+ tail = node.body[-1] if isinstance(node.body, list) and node.body else None
204
+ return tail if isinstance(tail, (IfStatement, SwitchStatement)) and _has_untaken_arm(tail) else None
205
+
206
+
207
+ def _tail_reaches_loop(body) -> bool:
208
+ """True when a statement list's value comes from a loop: its tail is a
209
+ loop, or a tail if/switch has an arm whose own tail reaches one."""
210
+ if not isinstance(body, list) or not body:
211
+ return False
212
+ tail = body[-1]
213
+ if isinstance(tail, LOOPS):
214
+ return True
215
+ if isinstance(tail, (IfStatement, SwitchStatement)):
216
+ return any(_tail_reaches_loop(arm) for arm in _arms(tail))
217
+ return False
218
+
219
+
220
+ # ---------------------------------------------------------------- arm values
221
+
222
+ def _arms(value) -> list:
223
+ """The arms whose tails give `value` its value: the branches of an if or a
224
+ switch (expression or statement), the body of a loop. A missing arm is None."""
225
+ if isinstance(value, IfExpression):
226
+ return [value.true_expr, value.false_expr]
227
+ if isinstance(value, IfStatement):
228
+ return [value.body] + [body for _, body in value.elseifs] + [value.else_body]
229
+ if isinstance(value, (SwitchExpression, SwitchStatement)):
230
+ return [body for _, body in value.cases] + [value.default]
231
+ if isinstance(value, LOOPS):
232
+ return [value.body]
233
+ return []
234
+
235
+
236
+ def _value_tails(value):
237
+ """Each plain value `value` can yield, through the tails of its arms."""
238
+ arms = _arms(value)
239
+ if not arms:
240
+ yield value
241
+ for arm in arms:
242
+ if arm:
243
+ yield from _value_tails(_tail_expression(arm))
244
+
245
+
246
+ def _has_untaken_arm(value) -> bool:
247
+ """True when an arm of `value`, at any depth, may not run: an if without
248
+ else, a switch without a default."""
249
+ return any(not arm or _has_untaken_arm(_tail_expression(arm)) for arm in _arms(value))
250
+
251
+
252
+ def _is_bool_value(value) -> bool:
253
+ if isinstance(value, Literal):
254
+ return value.literal_type == 'bool'
255
+ if isinstance(value, Identifier):
256
+ return value.name in ('true', 'false')
257
+ if isinstance(value, BinaryOp):
258
+ return value.op in BOOL_OPERATORS
259
+ return isinstance(value, UnaryOp) and value.op == 'not'
260
+
261
+
262
+ def yields_bool(value) -> bool:
263
+ """True when an arm of `value` yields a bool by its syntax. Only the slot of
264
+ a loop that runs no iteration reads it: Pine's guide gives false to an
265
+ untaken if/switch arm, which the bool typing decides (card #56), and says
266
+ nothing of a loop, so that slot stays as #50 left it (UNVERIFIED)."""
267
+ return any(_is_bool_value(tail) for tail in _value_tails(value))
268
+
269
+
270
+ def bool_is_never_na(script: Script) -> bool:
271
+ """Pine v6 removed na from bool; before v6 an untaken bool arm is na. A
272
+ script without a version annotation is taken as v6, the transpiler's target."""
273
+ version = script.version.version if script.version else ''
274
+ return not version.isdigit() or int(version) >= 6
275
+
276
+
277
+ # Pine v4 and v5 keep a third bool state, na; the runtime names exactly these two versions (`na_bool`)
278
+ THREE_STATE_BOOL_VERSIONS = ('4', '5')
279
+
280
+
281
+ def keeps_three_state_bool(script: Script) -> bool:
282
+ """True for a Pine v4 or v5 script: its bool may be na (`bool b = na`, a bool history before
283
+ warm-up), so its decorator sets the runtime's `na_bool` switch (card #99, item 5; MEASURED on
284
+ TradingView 2026-10-06, probe6). A script with no version annotation is v6, as in `bool_is_never_na`."""
285
+ return bool(script.version) and script.version.version in THREE_STATE_BOOL_VERSIONS
286
+
287
+
288
+ def _tuple_width(value, functions: '_UserFunctions') -> Optional[int]:
289
+ """The width of the tuples `value` yields, from an arm that ends in `[x, y]`,
290
+ in a tuple destructure, or in a call of a user function of known width."""
291
+ for tail in _value_tails(value):
292
+ if isinstance(tail, ArrayLiteral):
293
+ return len(tail.elements)
294
+ if isinstance(tail, Assignment) and isinstance(tail.target, TupleDestructure):
295
+ return len(tail.target.names)
296
+ if isinstance(tail, FunctionCall) and functions.width_of(tail):
297
+ return functions.width_of(tail)
298
+ return None
299
+
300
+
301
+ def _destructured_calls(value) -> list:
302
+ """The calls whose tuple a `[a, b] = value` destructures: the value itself,
303
+ either arm of a ternary, the tail of each arm of an if/switch or of a
304
+ loop's body, or the expression `request.security` evaluates. A call nested in an argument
305
+ yields a scalar and is not one of them."""
306
+ if isinstance(value, TernaryOp):
307
+ return _destructured_calls(value.true_expr) + _destructured_calls(value.false_expr)
308
+ if isinstance(value, VALUE_EXPRESSIONS + LOOPS):
309
+ return [call for tail in _value_tails(value) for call in _destructured_calls(tail)]
310
+ if not isinstance(value, FunctionCall):
311
+ return []
312
+ if value.func == 'request.security':
313
+ positional = value.args[2] if len(value.args) > 2 else None
314
+ return _destructured_calls(value.kwargs.get('expression', positional))
315
+ return [value]
316
+
317
+
318
+ @dataclasses.dataclass(frozen=True)
319
+ class _UserFunctions:
320
+ """What the tuple fall-through needs to know of the script's functions."""
321
+ names: frozenset = frozenset()
322
+ methods: frozenset = frozenset()
323
+ widths: Dict[str, int] = dataclasses.field(default_factory=dict) # name -> tuple width
324
+ destructured: frozenset = frozenset() # the functions a `[a, b] = …` value calls
325
+
326
+ @classmethod
327
+ def of(cls, script: Script) -> '_UserFunctions':
328
+ declared = [node for node in script.declarations if isinstance(node, FuncDecl)]
329
+ known = cls(names=frozenset(function.name for function in declared),
330
+ methods=frozenset(function.name for function in declared if function.is_method))
331
+ # Pine declares a function before its callers, so widths build in order
332
+ for function in declared:
333
+ width = _tuple_width(_tail_expression(function.body), known)
334
+ if width:
335
+ known = dataclasses.replace(known, widths={**known.widths, function.name: width})
336
+ destructured_calls = [call for node in walk(script)
337
+ if isinstance(node, Assignment) and isinstance(node.target, TupleDestructure)
338
+ for call in _destructured_calls(node.value)]
339
+ destructured = frozenset(filter(None, map(known.called, destructured_calls)))
340
+ return dataclasses.replace(known, destructured=destructured)
341
+
342
+ def called(self, call: FunctionCall) -> Optional[str]:
343
+ """The user function a call runs: `f(…)`, or a method as `t.f(…)`."""
344
+ if not isinstance(call.func, str):
345
+ return None
346
+ if call.func in self.names:
347
+ return call.func
348
+ _, dot, method = call.func.rpartition('.')
349
+ return method if dot and method in self.methods else None
350
+
351
+ def width_of(self, call: FunctionCall) -> Optional[int]:
352
+ return self.widths.get(self.called(call))
353
+
354
+
355
+ @dataclasses.dataclass(frozen=True)
356
+ class _Untaken:
357
+ """The value of an arm that does not run, and of a loop with no iteration:
358
+ a tuple of na as wide as the value's tuples, false for a bool, else na.
359
+ An arm is bool by the value's type, a loop's empty slot by its syntax."""
360
+ tuple_width: Optional[int] = None
361
+ is_bool: bool = False
362
+ never_run_is_bool: bool = False
363
+
364
+ def value(self, line: int) -> Expression:
365
+ if self.tuple_width:
366
+ return ArrayLiteral(elements=[NaLiteral(line=line) for _ in range(self.tuple_width)], line=line)
367
+ if self.is_bool:
368
+ return Literal(value=False, literal_type='bool', line=line)
369
+ return NaLiteral(line=line)
370
+
371
+ def never_run_value(self, line: int) -> Expression:
372
+ """The value of a loop that runs no iteration."""
373
+ return dataclasses.replace(self, is_bool=self.never_run_is_bool).value(line)
374
+
375
+
376
+ class _BlockValueLowering:
377
+ def __init__(self, used_names, functions: _UserFunctions, bool_is_never_na: bool):
378
+ self.used_names = used_names
379
+ self.functions = functions
380
+ self.bool_is_never_na = bool_is_never_na
381
+ self.line = 0 # nearest enclosing source line, for refusals and new nodes
382
+
383
+ def refuse(self, feature: str):
384
+ raise UnsupportedFeatureError(feature, self.line)
385
+
386
+ def refuse_nested(self, value):
387
+ """A block value left inside an expression cannot be emitted."""
388
+ if _contains_block_value(value):
389
+ self.refuse('an if/switch expression with a multi-statement branch, a default-only switch with '
390
+ 'a subject, or a loop, inside another expression (assign it to a variable first)')
391
+
392
+ def untaken_of(self, value, value_is_bool: bool) -> _Untaken:
393
+ """`value_is_bool` is the stamp of the if/switch/loop, or of the
394
+ function, whose value `value` is (bool_value_typing, card #56)."""
395
+ return _Untaken(tuple_width=_tuple_width(value, self.functions), is_bool=value_is_bool,
396
+ never_run_is_bool=self.bool_is_never_na and yields_bool(value))
397
+
398
+ # ---------------------------------------------------------------- values
399
+
400
+ def value_into(self, expression, write: Write, untaken: Optional[_Untaken] = None) -> list:
401
+ """Statements that compute `expression` and pass its value to `write`.
402
+ Already in statement position, so every if/switch becomes statements:
403
+ a tail if statement turned into an expression holds statements too.
404
+ `untaken` is what an arm that does not run yields; one if/switch/loop
405
+ has one type, so the outermost decides it for every nested arm."""
406
+ if isinstance(expression, VALUE_EXPRESSIONS + LOOPS):
407
+ untaken = untaken or self.untaken_of(expression, expression.value_is_bool)
408
+ if isinstance(expression, LOOPS):
409
+ return self.loop_into(expression, write, untaken)
410
+ if isinstance(expression, IfExpression):
411
+ self.refuse_nested(expression.condition)
412
+ return [IfStatement(condition=expression.condition,
413
+ body=self.branch_into(expression.true_expr, write, untaken),
414
+ elseifs=[],
415
+ else_body=self.branch_into(expression.false_expr, write, untaken),
416
+ line=self.line)]
417
+ if isinstance(expression, SwitchExpression):
418
+ return self.switch_into(expression, write, untaken)
419
+ self.refuse_nested(expression)
420
+ return [write(expression)]
421
+
422
+ def loop_into(self, loop, write: Write, untaken: _Untaken) -> list:
423
+ """`slot = <untaken>`, the loop with each value its body's tail yields
424
+ written to the slot, then `write(slot)`. The slot is fresh, so the body
425
+ still reads the target's old value; an iteration that breaks or
426
+ continues before its tail writes nothing, so the slot keeps the last
427
+ value an iteration evaluated (Pine docs, Loops)."""
428
+ self.refuse_nested([getattr(loop, name, None) for name in LOOP_HEADER_FIELDS])
429
+ slot = fresh_name('loop_value', self.used_names)
430
+ body = self.branch_into(loop.body, self.local_writer(slot), untaken)
431
+ return [Assignment(target=slot, value=untaken.never_run_value(self.line), line=self.line),
432
+ dataclasses.replace(loop, body=body, line=loop.line or self.line),
433
+ write(Identifier(name=slot, line=self.line))]
434
+
435
+ def switch_into(self, switch: SwitchExpression, write: Write, untaken: _Untaken) -> list:
436
+ """An if / else-if chain; a subject is bound once, then compared per case."""
437
+ self.refuse_nested([switch.expr] + [case for case, _ in switch.cases])
438
+ statements = []
439
+ if switch.expr is not None:
440
+ subject = fresh_name('switch_subject', self.used_names)
441
+ statements.append(Assignment(target=subject, value=switch.expr, line=self.line))
442
+
443
+ def condition_of(case):
444
+ return BinaryOp(left=Identifier(name=subject, line=self.line), op='==', right=case, line=self.line)
445
+ else:
446
+ def condition_of(case):
447
+ return case
448
+ if not switch.cases:
449
+ return statements + self.branch_into(switch.default, write, untaken)
450
+ (first_case, first_body), *other_cases = switch.cases
451
+ return statements + [IfStatement(
452
+ condition=condition_of(first_case),
453
+ body=self.branch_into(first_body, write, untaken),
454
+ elseifs=[(condition_of(case), self.branch_into(body, write, untaken)) for case, body in other_cases],
455
+ else_body=self.branch_into(switch.default, write, untaken), line=self.line)]
456
+
457
+ def branch_into(self, branch, write: Write, untaken: _Untaken) -> list:
458
+ """Statements that run a branch and pass its value to `write`; a missing
459
+ branch yields `untaken`."""
460
+ if not branch:
461
+ return [write(untaken.value(self.line))]
462
+ if isinstance(branch, LOOPS):
463
+ branch = [branch]
464
+ if not isinstance(branch, list):
465
+ return self.value_into(branch, write, untaken)
466
+ *leading, tail = branch
467
+ statements = self.statements(leading)
468
+ if isinstance(tail, ExpressionStatement):
469
+ return statements + self.value_into(tail.expr, write, untaken)
470
+ if isinstance(tail, Expression):
471
+ return statements + self.value_into(tail, write, untaken)
472
+ if isinstance(tail, BINDINGS) and _target_name(tail):
473
+ # The value of `z = ...` or `z := ...` is z
474
+ return statements + self.statements([tail]) + [write(Identifier(name=_target_name(tail), line=self.line))]
475
+ if isinstance(tail, Assignment) and isinstance(tail.target, TupleDestructure):
476
+ # The value of `[a, b] = ...` is the tuple
477
+ names = [Identifier(name=name, line=self.line) for name in tail.target.names]
478
+ return statements + self.statements([tail]) + [write(ArrayLiteral(elements=names, line=self.line))]
479
+ if isinstance(tail, VALUELESS_TAILS):
480
+ return statements + self.value_into(self.as_value(tail), write, untaken)
481
+ if isinstance(tail, (BreakStatement, ContinueStatement)):
482
+ return statements + [tail] # this iteration yields no value: the slot keeps the last one
483
+ self.refuse(f'{type(tail).__name__} as the value of an if/switch expression branch')
484
+
485
+ @classmethod
486
+ def as_value(cls, statement):
487
+ """A tail if/switch statement as the value expression it yields; a loop as itself."""
488
+ if isinstance(statement, IfStatement):
489
+ return cls.if_statement_as_value(statement)
490
+ if isinstance(statement, SwitchStatement):
491
+ return SwitchExpression(expr=statement.expr, cases=statement.cases, default=statement.default,
492
+ line=statement.line)
493
+ return statement
494
+
495
+ @staticmethod
496
+ def if_statement_as_value(statement: IfStatement) -> IfExpression:
497
+ """`if a … else if b … else …` as nested if expressions, keeping every else-if."""
498
+ else_branch = statement.else_body
499
+ for condition, body in reversed(statement.elseifs):
500
+ else_branch = IfExpression(condition=condition, true_expr=body, false_expr=else_branch,
501
+ line=statement.line)
502
+ return IfExpression(condition=statement.condition, true_expr=statement.body,
503
+ false_expr=else_branch, line=statement.line)
504
+
505
+ def writer(self, target: str) -> Write:
506
+ line = self.line
507
+ return lambda value: Reassignment(target=target, value=value, line=line)
508
+
509
+ def local_writer(self, name: str) -> Write:
510
+ """`name = v` for a fresh local: `:=` would make it a Series in a function."""
511
+ line = self.line
512
+ return lambda value: Assignment(target=name, value=value, line=line)
513
+
514
+ def yielder(self) -> Write:
515
+ """A function tail's value as its last expression statement."""
516
+ line = self.line
517
+ return lambda value: ExpressionStatement(expr=value, line=line)
518
+
519
+ # ---------------------------------------------------------------- statements
520
+
521
+ def statements(self, statements) -> list:
522
+ lowered = []
523
+ for statement in statements:
524
+ lowered.extend(self.statement(statement))
525
+ return lowered
526
+
527
+ def block(self, body):
528
+ return None if body is None else self.statements(body)
529
+
530
+ def statement(self, node) -> list:
531
+ enclosing_line = self.line
532
+ self.line = node.line or self.line
533
+ try:
534
+ return self.lowered_statement(node)
535
+ finally:
536
+ self.line = enclosing_line
537
+
538
+ def lowered_statement(self, node) -> list:
539
+ replace = dataclasses.replace
540
+ value = getattr(node, 'value', None)
541
+ if isinstance(node, Assignment) and isinstance(node.target, TupleDestructure) and (
542
+ _is_block_value(value) or _has_untaken_arm(value)):
543
+ return self.tuple_statement(node)
544
+ if isinstance(node, BINDINGS) and _is_block_value(value):
545
+ return self.block_value_statement(node)
546
+ if _is_compound_block(node):
547
+ return self.compound_statement(node)
548
+ if isinstance(node, IfStatement):
549
+ self.refuse_nested([node.condition] + [condition for condition, _ in node.elseifs])
550
+ return [replace(node, body=self.block(node.body),
551
+ elseifs=[(condition, self.block(body)) for condition, body in node.elseifs],
552
+ else_body=self.block(node.else_body))]
553
+ if isinstance(node, SwitchStatement):
554
+ self.refuse_nested([node.expr] + [case for case, _ in node.cases])
555
+ return [replace(node, cases=[(case, self.block(body)) for case, body in node.cases],
556
+ default=self.block(node.default))]
557
+ if isinstance(node, LOOPS):
558
+ self.refuse_nested([getattr(node, name, None) for name in LOOP_HEADER_FIELDS])
559
+ return [replace(node, body=self.block(node.body))]
560
+ if isinstance(node, FuncDecl):
561
+ return [self.function(node)]
562
+ self.refuse_nested(node)
563
+ return [node]
564
+
565
+ def block_value_statement(self, node) -> list:
566
+ target = _target_name(node)
567
+ if isinstance(node, Assignment):
568
+ declaration = Assignment(target=target, value=NaLiteral(line=self.line),
569
+ type_hint=node.type_hint, line=self.line)
570
+ return [declaration] + self.value_into(node.value, self.writer(target))
571
+ if isinstance(node, Reassignment):
572
+ return self.value_into(node.value, self.writer(target))
573
+ return self.run_once_declaration(node)
574
+
575
+ def compound_statement(self, node: Reassignment) -> list:
576
+ """`s op= <block>`: s is read first, then the block runs into a fresh
577
+ local, then `s := <s as read> op <block value>`. Reading s first is
578
+ premise P1, UNVERIFIED on TradingView (card #51)."""
579
+ operation = node.value
580
+ left_operand = fresh_name('left_operand', self.used_names)
581
+ block_value = fresh_name('block_value', self.used_names)
582
+ computing = self.value_into(operation.right, self.local_writer(block_value))
583
+ return ([Assignment(target=left_operand, value=operation.left, line=self.line),
584
+ Assignment(target=block_value, value=NaLiteral(line=self.line), line=self.line)]
585
+ + computing
586
+ + [dataclasses.replace(node, value=dataclasses.replace(
587
+ operation, left=Identifier(name=left_operand, line=self.line),
588
+ right=Identifier(name=block_value, line=self.line)))])
589
+
590
+ def tuple_statement(self, node: Assignment) -> list:
591
+ """`[a, b] = <if/switch/loop>`: the value goes to a fresh local that
592
+ starts as a tuple of na as wide as the target, as does an arm that
593
+ does not run; the target destructures it once (card #50)."""
594
+ names = list(node.target.names)
595
+ untaken = _Untaken(tuple_width=len(names))
596
+ slot = fresh_name('tuple_value', self.used_names)
597
+ computing = self.value_into(node.value, self.local_writer(slot), untaken)
598
+ destructure = Assignment(
599
+ target=TupleDestructure(names=names, value=Identifier(name=slot, line=self.line), line=self.line),
600
+ value=Identifier(name=slot, line=self.line), type_hint=node.type_hint, line=self.line)
601
+ return [Assignment(target=slot, value=untaken.value(self.line), line=self.line)] + computing + [destructure]
602
+
603
+ def run_once_declaration(self, node) -> list:
604
+ """`var x = if …` -> `var x = na`, `var bool flag = false`, and
605
+ `if not flag` { `flag := true`; <lowered value> }: the value runs on the
606
+ first bar only, like a var initialiser."""
607
+ kind = type(node)
608
+ flag = fresh_name(f'{node.name}_initialised', self.used_names)
609
+ declarations = [kind(name=node.name, value=NaLiteral(line=self.line), type_hint=node.type_hint,
610
+ line=self.line),
611
+ kind(name=flag, value=Literal(value=False, literal_type='bool', line=self.line),
612
+ type_hint='bool', line=self.line)]
613
+ first_bar_only = IfStatement(
614
+ condition=UnaryOp(op='not', operand=Identifier(name=flag, line=self.line), line=self.line),
615
+ body=[Reassignment(target=flag, value=Literal(value=True, literal_type='bool', line=self.line),
616
+ line=self.line)] + self.value_into(node.value, self.writer(node.name)),
617
+ elseifs=[], else_body=None, line=self.line)
618
+ return declarations + [first_bar_only]
619
+
620
+ def function(self, node: FuncDecl) -> FuncDecl:
621
+ replace = dataclasses.replace
622
+ if not isinstance(node.body, list):
623
+ if not has_block_branch(node.body):
624
+ self.refuse_nested(node.body)
625
+ return node
626
+ # `f() => if …`: the value into a fresh local, which the function yields
627
+ result = fresh_name(f'{node.name}_result', self.used_names)
628
+ body = self.statement(Assignment(target=result, value=node.body, line=self.line))
629
+ return replace(node, body=body + [ExpressionStatement(expr=Identifier(name=result, line=self.line),
630
+ line=self.line)])
631
+ if node.body and self.tail_is_lowered(node):
632
+ # every arm of the tail ends in the expression it yields, which the
633
+ # transformer's tail walk returns (cards #53, #54)
634
+ *leading, tail = node.body
635
+ lowered_leading = self.statements(leading)
636
+ enclosing_line, self.line = self.line, tail.line or self.line
637
+ try:
638
+ value = self.as_value(tail)
639
+ untaken = self.untaken_of(value, node.value_is_bool)
640
+ return replace(node, body=lowered_leading + self.value_into(value, self.yielder(), untaken))
641
+ finally:
642
+ self.line = enclosing_line
643
+ return replace(node, body=self.body_with_its_value(node.body))
644
+
645
+ def tail_is_lowered(self, node: FuncDecl) -> bool:
646
+ """A tail the transformer cannot return by itself: one that reaches a
647
+ loop, or a tuple-returning if/switch with an arm that may not run,
648
+ which yields a tuple of na (card #54). A function the script
649
+ destructures, whose tuple width is unknown, is refused."""
650
+ if _tail_reaches_loop(node.body):
651
+ return True
652
+ tail = _untaken_tail(node)
653
+ if tail is None:
654
+ return False
655
+ if _tuple_width(tail, self.functions):
656
+ return True
657
+ if node.name in self.functions.destructured:
658
+ self.line = tail.line or self.line
659
+ self.refuse(f'the tuple function {node.name} has an if/switch tail without else, and its '
660
+ 'tuple width is unknown (give it an else that returns the tuple)')
661
+ return False
662
+
663
+ def body_with_its_value(self, body: list) -> list:
664
+ """Lower a function body whose value is its last statement. When that
665
+ statement binds a block value, the lowered body ends in the if
666
+ statement that writes it, so the bound name is appended as the value."""
667
+ lowered = self.statements(body)
668
+ last = body[-1] if body else None
669
+ if isinstance(last, BINDINGS) and _target_name(last) and _is_block_value(last.value):
670
+ lowered.append(ExpressionStatement(expr=Identifier(name=_target_name(last), line=self.line),
671
+ line=self.line))
672
+ return lowered
673
+
674
+ # ---------------------------------------------------------------- script
675
+
676
+ def script(self, script: Script) -> Script:
677
+ declarations: List = []
678
+ first_bar_initialisers: List = []
679
+ for declaration in script.declarations:
680
+ enclosing_line, self.line = self.line, declaration.line or self.line
681
+ if isinstance(declaration, (VarDecl, VaripDecl)) and _is_block_value(declaration.value):
682
+ *declared, first_bar_only = self.run_once_declaration(declaration)
683
+ declarations.extend(declared)
684
+ # Same line as the declaration: the transformer merges declarations
685
+ # and body by line, so this runs right after it
686
+ first_bar_initialisers.append(first_bar_only)
687
+ elif isinstance(declaration, FuncDecl):
688
+ declarations.append(self.function(declaration))
689
+ else:
690
+ self.refuse_nested(declaration)
691
+ declarations.append(declaration)
692
+ self.line = enclosing_line
693
+ body = self.statements(script.body) + first_bar_initialisers
694
+ return dataclasses.replace(script, declarations=declarations, body=body)