pine2pyne 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pine2pyne/__init__.py +84 -0
- pine2pyne/__main__.py +12 -0
- pine2pyne/ast_nodes.py +405 -0
- pine2pyne/ast_walk.py +93 -0
- pine2pyne/block_value_lowering.py +694 -0
- pine2pyne/bool_builtins.py +42 -0
- pine2pyne/bool_value_typing.py +580 -0
- pine2pyne/cli.py +247 -0
- pine2pyne/codegen.py +886 -0
- pine2pyne/compat/__init__.py +19 -0
- pine2pyne/compat/__main__.py +6 -0
- pine2pyne/compat/cli.py +114 -0
- pine2pyne/compat/comparison.py +105 -0
- pine2pyne/compat/corpus.py +73 -0
- pine2pyne/compat/report.py +95 -0
- pine2pyne/compat/runner.py +63 -0
- pine2pyne/condition_typing.py +260 -0
- pine2pyne/conditional_history_calls.py +96 -0
- pine2pyne/core/__init__.py +26 -0
- pine2pyne/core/contracts.py +125 -0
- pine2pyne/di.py +173 -0
- pine2pyne/emit/__init__.py +5 -0
- pine2pyne/emit/legacy.py +14 -0
- pine2pyne/emit/pynecomp/__init__.py +20 -0
- pine2pyne/emit/pynecomp/banner.py +30 -0
- pine2pyne/emit/pynecomp/blank_lines.py +69 -0
- pine2pyne/emit/pynecomp/emitter.py +61 -0
- pine2pyne/emit/pynecomp/imports.py +65 -0
- pine2pyne/emit/pynecomp/passes/__init__.py +84 -0
- pine2pyne/emit/pynecomp/passes/annotations.py +422 -0
- pine2pyne/emit/pynecomp/passes/base.py +69 -0
- pine2pyne/emit/pynecomp/passes/builtins.py +169 -0
- pine2pyne/emit/pynecomp/passes/hoisting.py +205 -0
- pine2pyne/emit/pynecomp/passes/imports.py +112 -0
- pine2pyne/emit/pynecomp/passes/legacy_colors.py +101 -0
- pine2pyne/emit/pynecomp/passes/library.py +205 -0
- pine2pyne/emit/pynecomp/passes/rewrites.py +215 -0
- pine2pyne/emit/pynecomp/printer.py +288 -0
- pine2pyne/emit/pynecomp/unparser.py +186 -0
- pine2pyne/emitted_names.py +152 -0
- pine2pyne/errors.py +69 -0
- pine2pyne/for_loop_end_bound.py +172 -0
- pine2pyne/grouped_history_lowering.py +310 -0
- pine2pyne/import_resolver.py +469 -0
- pine2pyne/input_hoisting.py +195 -0
- pine2pyne/legacy_function_names.py +74 -0
- pine2pyne/legacy_sessions.py +68 -0
- pine2pyne/lexer.py +630 -0
- pine2pyne/library_collision.py +434 -0
- pine2pyne/namespace_members.py +247 -0
- pine2pyne/node_ids.py +235 -0
- pine2pyne/once_lowering.py +75 -0
- pine2pyne/overload_resolution.py +475 -0
- pine2pyne/parse_repair.py +398 -0
- pine2pyne/parser.py +1873 -0
- pine2pyne/pine_builtins.py +394 -0
- pine2pyne/pine_semantics.py +604 -0
- pine2pyne/pipeline/__init__.py +18 -0
- pine2pyne/pipeline/service.py +70 -0
- pine2pyne/pipeline/stages.py +73 -0
- pine2pyne/scope_renamer.py +965 -0
- pine2pyne/strategy_when_lowering.py +101 -0
- pine2pyne/strict_boolean.py +101 -0
- pine2pyne/symbol_table.py +166 -0
- pine2pyne/tokens.py +143 -0
- pine2pyne/transformer.py +2510 -0
- pine2pyne/type_inference.py +422 -0
- pine2pyne/udt_field_defaults.py +149 -0
- pine2pyne/value_blocks.py +357 -0
- pine2pyne-0.2.4.dist-info/METADATA +99 -0
- pine2pyne-0.2.4.dist-info/RECORD +75 -0
- pine2pyne-0.2.4.dist-info/WHEEL +5 -0
- pine2pyne-0.2.4.dist-info/entry_points.txt +3 -0
- pine2pyne-0.2.4.dist-info/licenses/LICENSE +201 -0
- pine2pyne-0.2.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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)
|