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,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The pynecore.lib builtins whose value is of type bool, and the scalar type of
|
|
3
|
+
each builtin series, as the runtime annotates them.
|
|
4
|
+
|
|
5
|
+
GENERATED by tools/collect_bool_builtins.py from opencode-pyneruntime 6.9.4; do not
|
|
6
|
+
edit by hand. In Pine v6 an untaken if/switch whose value is of type bool is
|
|
7
|
+
false, not na; bool_value_typing reads these to type a builtin (card #56).
|
|
8
|
+
"""
|
|
9
|
+
from typing import Dict, FrozenSet
|
|
10
|
+
|
|
11
|
+
BOOL_BUILTINS: FrozenSet[str] = frozenset({
|
|
12
|
+
'array.every', 'array.includes', 'array.some', 'barstate.isconfirmed',
|
|
13
|
+
'barstate.isfirst', 'barstate.ishistory', 'barstate.islast',
|
|
14
|
+
'barstate.islastconfirmedhistory', 'barstate.isnew', 'barstate.isrealtime',
|
|
15
|
+
'chart.is_heikinashi', 'chart.is_kagi', 'chart.is_linebreak', 'chart.is_pnf',
|
|
16
|
+
'chart.is_range', 'chart.is_renko', 'chart.is_standard', 'map.contains',
|
|
17
|
+
'matrix.is_antidiagonal', 'matrix.is_antisymmetric', 'matrix.is_binary',
|
|
18
|
+
'matrix.is_diagonal', 'matrix.is_identity', 'matrix.is_square', 'matrix.is_stochastic',
|
|
19
|
+
'matrix.is_symmetric', 'matrix.is_triangular', 'matrix.is_zero', 'na',
|
|
20
|
+
'session.isfirstbar', 'session.isfirstbar_regular', 'session.islastbar',
|
|
21
|
+
'session.islastbar_regular', 'session.ismarket', 'session.ispostmarket',
|
|
22
|
+
'session.ispremarket', 'str.contains', 'str.endswith', 'str.startswith', 'ta.cross',
|
|
23
|
+
'ta.crossover', 'ta.crossunder', 'ta.falling', 'ta.rising', 'timeframe.change',
|
|
24
|
+
'timeframe.isdaily', 'timeframe.isdwm', 'timeframe.isintraday', 'timeframe.isminutes',
|
|
25
|
+
'timeframe.ismonthly', 'timeframe.isseconds', 'timeframe.isticks', 'timeframe.isweekly',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
BUILTIN_SERIES_TYPES: Dict[str, str] = {
|
|
29
|
+
'ask': 'float',
|
|
30
|
+
'bar_index': 'int',
|
|
31
|
+
'bid': 'float',
|
|
32
|
+
'close': 'float',
|
|
33
|
+
'high': 'float',
|
|
34
|
+
'hl2': 'float',
|
|
35
|
+
'hlc3': 'float',
|
|
36
|
+
'hlcc4': 'float',
|
|
37
|
+
'last_bar_index': 'int',
|
|
38
|
+
'low': 'float',
|
|
39
|
+
'ohlc4': 'float',
|
|
40
|
+
'open': 'float',
|
|
41
|
+
'volume': 'float',
|
|
42
|
+
}
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pine v6 bool typing of the values an untaken arm can default (card #56).
|
|
3
|
+
|
|
4
|
+
In Pine v6 an if/switch whose value is of TYPE bool gives false for an arm
|
|
5
|
+
that does not run, not na (migration guide to v6). The type comes from the
|
|
6
|
+
tail's syntax, a declared or inferred name, a builtin, a UDT field, the
|
|
7
|
+
element of an `array<bool>`, or a user function's own tail. This pass decides
|
|
8
|
+
it once, on the renamed script and before the block-value lowering rewrites it, and
|
|
9
|
+
stamps `value_is_bool` on each if/switch expression, loop and function whose
|
|
10
|
+
value is bool. Every later emit site (the block-value pre-pass, the ternary
|
|
11
|
+
emitter, the function-tail return) reads the stamp. Unknown is not bool: it
|
|
12
|
+
stays na.
|
|
13
|
+
|
|
14
|
+
One walk records the scope of every value (Pine's scopes, as the overload
|
|
15
|
+
resolution builds them: `rename_shadowing_locals` runs first). Each question
|
|
16
|
+
("is this value, function or binding bool?") is then a formula over other
|
|
17
|
+
questions, built once. The formulas are solved together as a least fixpoint,
|
|
18
|
+
so a chain of untyped names of any length is followed without recursion and
|
|
19
|
+
without a bound, and a cycle (recursion is not valid Pine) is not bool.
|
|
20
|
+
An if/switch/loop in the tail of another value or of a function has the
|
|
21
|
+
same type as it: Pine gives one if/switch one type.
|
|
22
|
+
"""
|
|
23
|
+
import dataclasses
|
|
24
|
+
from typing import Dict, List, Optional, Tuple
|
|
25
|
+
|
|
26
|
+
from .ast_nodes import (
|
|
27
|
+
Assignment, BinaryOp, ExpressionStatement, ForInLoop, ForLoop, FuncDecl, FunctionCall, Identifier,
|
|
28
|
+
IfExpression, IfStatement, IndexAccess, InputDecl, Literal, MemberAccess, NaLiteral, Reassignment,
|
|
29
|
+
Script, SwitchExpression, SwitchStatement, TernaryOp, TupleDestructure, TypeDecl, UnaryOp, VarDecl,
|
|
30
|
+
VaripDecl,
|
|
31
|
+
)
|
|
32
|
+
from .ast_walk import CONTAINER_TYPES, LEAF_TYPES, child_field_names, map_children
|
|
33
|
+
from .block_value_lowering import BOOL_OPERATORS, LOOPS
|
|
34
|
+
from .bool_builtins import BOOL_BUILTINS, BUILTIN_SERIES_TYPES
|
|
35
|
+
from .namespace_members import NAMESPACE_MEMBERS
|
|
36
|
+
from .overload_resolution import (
|
|
37
|
+
Binding, BlockScope, DeclaredTypeResolver, bind_declaration, nested_blocks, overloads_taking,
|
|
38
|
+
)
|
|
39
|
+
from .type_inference import receiver_path
|
|
40
|
+
|
|
41
|
+
VALUE_NODES = (IfExpression, SwitchExpression) + LOOPS
|
|
42
|
+
BLOCK_STATEMENTS = (FuncDecl, IfStatement, SwitchStatement) + LOOPS
|
|
43
|
+
BINDINGS = (Assignment, VarDecl, VaripDecl, Reassignment)
|
|
44
|
+
# A Pine cast and an input of type bool; pynecore does not annotate them
|
|
45
|
+
BOOL_CONSTRUCTS = frozenset({'bool', 'input.bool'})
|
|
46
|
+
# The Pine namespace of each builtin receiver kind: `a.includes(1)` is `array.includes`
|
|
47
|
+
RECEIVER_NAMESPACES = {'array': 'array', 'map': 'map', 'matrix': 'matrix', 'string': 'str'}
|
|
48
|
+
# The array methods that return an element: bool on an `array<bool>`
|
|
49
|
+
ARRAY_ELEMENT_METHODS = frozenset({'get', 'first', 'last', 'pop'})
|
|
50
|
+
|
|
51
|
+
# A formula is True, False, ('ref', key), ('any', items) or ('all', items); a key
|
|
52
|
+
# is ('value' | 'function' | 'binding', id of the node or binding)
|
|
53
|
+
Key = Tuple[str, int]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def stamp_bool_values(script: Script) -> Script:
|
|
57
|
+
"""A copy of `script` with `value_is_bool=True` on every bool-valued
|
|
58
|
+
if/switch expression, loop and function; `script` itself when none is."""
|
|
59
|
+
typing = _BoolTyping(script)
|
|
60
|
+
stamped = {node_id for kind, node_id in typing.bool_keys() if kind != 'binding'}
|
|
61
|
+
if not stamped:
|
|
62
|
+
return script
|
|
63
|
+
return _Restamp(stamped, typing.ancestors_of(stamped)).rebuilt(script)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def reads_na_of_bool(script: Script) -> bool:
|
|
67
|
+
"""Whether a named bool value is read where it may be na: with history (`b[1]`) or
|
|
68
|
+
through `na(b)`. Pine v5 marks only such a script `na_bool=True`; the history of an
|
|
69
|
+
expression (`(a > b)[1]`) or of an untyped parameter does not count. Measured with
|
|
70
|
+
probe scripts compiled by PyneComp v6.0.80; 82 of the 327 v5 scripts of the reference."""
|
|
71
|
+
typing = _BoolTyping(script)
|
|
72
|
+
for name, scope in typing.na_calls:
|
|
73
|
+
binding = scope.lookup(name)
|
|
74
|
+
if binding is not None and _is_bool_collection(binding.declared_type):
|
|
75
|
+
return True # `na(a)` of an `array<bool>`, not its history: any Pine version (probes nb6_02, nb7_*)
|
|
76
|
+
if _reads_na_of_a_bool_collection_argument(script):
|
|
77
|
+
return True
|
|
78
|
+
if _declared_version(script) != 5:
|
|
79
|
+
return False
|
|
80
|
+
formulas = [typing._name(name, scope) for name, scope in typing.na_reads]
|
|
81
|
+
if any(formula is True for formula in formulas):
|
|
82
|
+
return True
|
|
83
|
+
references = [reference for formula in formulas for reference in _refs(formula)]
|
|
84
|
+
if not references:
|
|
85
|
+
return False
|
|
86
|
+
true_keys = typing.bool_keys(tuple(references))
|
|
87
|
+
return any(_holds(formula, true_keys) for formula in formulas)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _is_bool_collection(declared_type) -> bool:
|
|
91
|
+
"""`array<bool>`, `matrix<bool>`, `map<K, bool>` (or `bool[]`)."""
|
|
92
|
+
if not isinstance(declared_type, str):
|
|
93
|
+
return False
|
|
94
|
+
compact = declared_type.replace(' ', '')
|
|
95
|
+
return compact in ('array<bool>', 'matrix<bool>', 'bool[]') or (compact.startswith('map<')
|
|
96
|
+
and compact.endswith(',bool>'))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _reads_na_of_a_bool_collection_argument(script: Script) -> bool:
|
|
100
|
+
"""`is_empty(arr) => na(arr) or ...` called with an `array<bool>`: the untyped parameter is a bool
|
|
101
|
+
collection where it is called with one (probe bis_isEmpty, PyneComp v6.0.80)."""
|
|
102
|
+
from .ast_walk import walk
|
|
103
|
+
from .ast_nodes import MethodCall, Parameter
|
|
104
|
+
nodes = list(walk(script))
|
|
105
|
+
collections = {node.name for node in nodes if isinstance(node, Parameter) and _is_bool_collection(node.type_hint)}
|
|
106
|
+
collections |= {getattr(node, 'name', None) or node.target for node in nodes
|
|
107
|
+
if isinstance(node, (VarDecl, VaripDecl, Assignment))
|
|
108
|
+
and _is_bool_collection(getattr(node, 'type_hint', None))
|
|
109
|
+
and isinstance(getattr(node, 'name', None) or node.target, str)}
|
|
110
|
+
if not collections:
|
|
111
|
+
return False
|
|
112
|
+
functions = {node.name: node for node in nodes if isinstance(node, FuncDecl)}
|
|
113
|
+
flagged = set()
|
|
114
|
+
for node in nodes:
|
|
115
|
+
if isinstance(node, FunctionCall) and isinstance(node.func, str) and node.func in functions:
|
|
116
|
+
for position, argument in enumerate(node.args):
|
|
117
|
+
parameters = functions[node.func].params
|
|
118
|
+
if isinstance(argument, Identifier) and argument.name in collections and position < len(parameters) \
|
|
119
|
+
and not parameters[position].type_hint:
|
|
120
|
+
flagged.add((node.func, parameters[position].name))
|
|
121
|
+
for function_name, parameter in flagged:
|
|
122
|
+
for node in walk(functions[function_name].body):
|
|
123
|
+
if isinstance(node, FunctionCall) and (isinstance(node.func, NaLiteral) or node.func == 'na') \
|
|
124
|
+
and node.args and isinstance(node.args[0], Identifier) and node.args[0].name == parameter:
|
|
125
|
+
return True
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _declared_version(script: Script) -> int:
|
|
130
|
+
text = getattr(script.version, 'version', '') if script.version else ''
|
|
131
|
+
return int(text) if str(text).isdigit() else 6
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def has_untaken_bool_switch(script: Script) -> bool:
|
|
135
|
+
"""Whether a `switch` without a default arm gives a bool: an untaken switch is na, so a Pine v5
|
|
136
|
+
script that has one is `na_bool=True` (probes nb3_02..04, PyneComp v6.0.80). A switch used as
|
|
137
|
+
a value or as a statement whose arms end with a bool (`x := true`) counts; an `if` does not."""
|
|
138
|
+
switches = [node for node in walk_nodes(script)
|
|
139
|
+
if isinstance(node, (SwitchStatement, SwitchExpression)) and node.default is None and node.cases]
|
|
140
|
+
if not switches:
|
|
141
|
+
return False
|
|
142
|
+
typing = _BoolTyping(script)
|
|
143
|
+
for switch in switches:
|
|
144
|
+
scope = typing.scope_of.get(id(switch))
|
|
145
|
+
for _, arm in switch.cases:
|
|
146
|
+
arm_scope = typing.block_scopes.get(id(arm), scope) if isinstance(arm, list) else scope
|
|
147
|
+
if arm_scope is None:
|
|
148
|
+
continue
|
|
149
|
+
tail = arm[-1] if isinstance(arm, list) and arm else arm
|
|
150
|
+
formula = typing._tail(tail, arm_scope, ('value', id(switch))) if isinstance(arm, list) \
|
|
151
|
+
else typing._expression(arm, arm_scope)
|
|
152
|
+
if formula is True or (formula and _holds(formula, typing.bool_keys(tuple(_refs(formula))))):
|
|
153
|
+
return True
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def walk_nodes(tree):
|
|
158
|
+
from .ast_walk import walk
|
|
159
|
+
return walk(tree)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _any(items) -> object:
|
|
163
|
+
items = tuple(item for item in items if item is not False)
|
|
164
|
+
if True in items:
|
|
165
|
+
return True
|
|
166
|
+
return (items[0] if len(items) == 1 else ('any', items)) if items else False
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _all(items) -> object:
|
|
170
|
+
items = tuple(item for item in items if item is not True)
|
|
171
|
+
if False in items:
|
|
172
|
+
return False
|
|
173
|
+
return (items[0] if len(items) == 1 else ('all', items)) if items else True
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _refs(formula) -> list:
|
|
177
|
+
if formula is True or formula is False:
|
|
178
|
+
return []
|
|
179
|
+
if formula[0] == 'ref':
|
|
180
|
+
return [formula[1]]
|
|
181
|
+
return [key for item in formula[1] for key in _refs(item)]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _holds(formula, true_keys: set) -> bool:
|
|
185
|
+
if formula is True or formula is False:
|
|
186
|
+
return formula
|
|
187
|
+
kind, content = formula
|
|
188
|
+
if kind == 'ref':
|
|
189
|
+
return content in true_keys
|
|
190
|
+
combine = any if kind == 'any' else all
|
|
191
|
+
return combine(_holds(item, true_keys) for item in content)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _statement_arms(statement) -> list:
|
|
195
|
+
"""The bodies whose tails give a tail if/switch statement its value."""
|
|
196
|
+
if isinstance(statement, IfStatement):
|
|
197
|
+
return [statement.body] + [body for _, body in statement.elseifs] + [statement.else_body]
|
|
198
|
+
return [body for _, body in statement.cases] + [statement.default]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _value_arms(value) -> list:
|
|
202
|
+
if isinstance(value, IfExpression):
|
|
203
|
+
return [value.true_expr, value.false_expr]
|
|
204
|
+
if isinstance(value, SwitchExpression):
|
|
205
|
+
return [body for _, body in value.cases] + [value.default]
|
|
206
|
+
return [value.body]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _statement_headers(statement) -> list:
|
|
210
|
+
"""The expressions of a block statement that run in the enclosing scope."""
|
|
211
|
+
if isinstance(statement, FuncDecl):
|
|
212
|
+
return list(statement.params)
|
|
213
|
+
if isinstance(statement, IfStatement):
|
|
214
|
+
return [statement.condition] + [condition for condition, _ in statement.elseifs]
|
|
215
|
+
if isinstance(statement, SwitchStatement):
|
|
216
|
+
return [statement.expr] + [case for case, _ in statement.cases]
|
|
217
|
+
if isinstance(statement, ForLoop):
|
|
218
|
+
return [statement.from_val, statement.to_val, statement.step]
|
|
219
|
+
if isinstance(statement, ForInLoop):
|
|
220
|
+
return [statement.iterable]
|
|
221
|
+
return [statement.condition]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _declared_container(type_name: Optional[str]):
|
|
225
|
+
"""(kind, element is bool) for a declared builtin container or string type."""
|
|
226
|
+
if type_name == 'string':
|
|
227
|
+
return 'string', False
|
|
228
|
+
kind, bracket, element = (type_name or '').partition('<')
|
|
229
|
+
if bracket and kind in ('array', 'map', 'matrix'):
|
|
230
|
+
return kind, kind == 'array' and element.rstrip('>').strip() == 'bool'
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class _BoolTyping:
|
|
235
|
+
"""The scopes of one script and the bool answer of each of its values."""
|
|
236
|
+
|
|
237
|
+
def __init__(self, script: Script):
|
|
238
|
+
self.functions: Dict[str, List[FuncDecl]] = {}
|
|
239
|
+
self.nodes: Dict[int, object] = {} # id -> value node, function or binding
|
|
240
|
+
self.scope_of: Dict[int, BlockScope] = {} # id(value node) -> the scope it is in
|
|
241
|
+
self.block_scopes: Dict[int, BlockScope] = {} # id(block list) -> its scope
|
|
242
|
+
self.function_scopes: Dict[int, BlockScope] = {} # id(function) -> its body's scope
|
|
243
|
+
self.contexts: Dict[int, list] = {} # id(value node) -> what its target says
|
|
244
|
+
self.tuple_elements: Dict[int, tuple] = {} # id(binding) -> (binding, destructured value, position)
|
|
245
|
+
self.implied_by: Dict[Key, List[Key]] = {} # a tail value -> the values it is the tail of
|
|
246
|
+
self.parents: Dict[int, list] = {}
|
|
247
|
+
self.na_reads: List[tuple] = [] # (name, scope): `x[n]` and `na(x)` of a named value
|
|
248
|
+
self.na_calls: List[tuple] = [] # (name, scope): `na(x)` only
|
|
249
|
+
udt_field_types = {declaration.name: {name: field_type for name, field_type, *_ in declaration.fields}
|
|
250
|
+
for declaration in script.declarations if isinstance(declaration, TypeDecl)}
|
|
251
|
+
self.resolver = DeclaredTypeResolver(lambda node: None, udt_field_types)
|
|
252
|
+
self._walk(script)
|
|
253
|
+
|
|
254
|
+
# ------------------------------------------------------------ the walk
|
|
255
|
+
def _walk(self, script: Script) -> None:
|
|
256
|
+
"""Visit every node once, giving each the scope it is in. Every file is
|
|
257
|
+
walked, so the loop makes no call per plain node (card #56 cost gate)."""
|
|
258
|
+
pending = [(script, BlockScope(), None)]
|
|
259
|
+
fields_by_type: Dict[type, tuple] = {}
|
|
260
|
+
while pending:
|
|
261
|
+
value, scope, parent = pending.pop()
|
|
262
|
+
value_type = type(value)
|
|
263
|
+
if value_type in CONTAINER_TYPES or value_type is dict:
|
|
264
|
+
items = value.values() if value_type is dict else value
|
|
265
|
+
pending.extend([(item, scope, parent) for item in items if type(item) not in LEAF_TYPES])
|
|
266
|
+
continue
|
|
267
|
+
fields = fields_by_type.get(value_type)
|
|
268
|
+
if fields is None:
|
|
269
|
+
is_node_type = hasattr(value_type, '__dataclass_fields__')
|
|
270
|
+
fields = fields_by_type[value_type] = child_field_names(value_type) if is_node_type else False
|
|
271
|
+
if fields is False:
|
|
272
|
+
continue
|
|
273
|
+
self.parents.setdefault(id(value), []).append(parent)
|
|
274
|
+
if isinstance(value, BLOCK_STATEMENTS + (IfExpression, SwitchExpression)):
|
|
275
|
+
self._enter_block(value, scope, pending)
|
|
276
|
+
continue
|
|
277
|
+
if isinstance(value, BINDINGS + (InputDecl,)):
|
|
278
|
+
self._bind(value, scope)
|
|
279
|
+
elif value_type is IndexAccess or value_type is FunctionCall:
|
|
280
|
+
self._record_na_read(value, scope)
|
|
281
|
+
pending.extend([(child, scope, value) for child in [getattr(value, name) for name in fields]
|
|
282
|
+
if type(child) not in LEAF_TYPES])
|
|
283
|
+
|
|
284
|
+
def _enter_block(self, value, scope: BlockScope, pending: list) -> None:
|
|
285
|
+
"""A function, if, switch or loop: its value and the scopes of its blocks."""
|
|
286
|
+
if not isinstance(value, (IfStatement, SwitchStatement)):
|
|
287
|
+
self.nodes[id(value)] = value
|
|
288
|
+
self.scope_of[id(value)] = scope
|
|
289
|
+
if isinstance(value, BLOCK_STATEMENTS):
|
|
290
|
+
self._enter_block_statement(value, scope, pending)
|
|
291
|
+
else:
|
|
292
|
+
self._enter_value_expression(value, scope, pending)
|
|
293
|
+
|
|
294
|
+
def _enter_block_statement(self, statement, scope: BlockScope, pending: list) -> None:
|
|
295
|
+
blocks = nested_blocks(statement, scope)
|
|
296
|
+
for body, block_scope in blocks:
|
|
297
|
+
self.block_scopes[id(body)] = block_scope
|
|
298
|
+
pending.append((body, block_scope, statement))
|
|
299
|
+
if isinstance(statement, FuncDecl):
|
|
300
|
+
self.functions.setdefault(statement.name, []).append(statement)
|
|
301
|
+
self.function_scopes[id(statement)] = blocks[0][1]
|
|
302
|
+
pending.extend((header, scope, statement) for header in _statement_headers(statement) if header is not None)
|
|
303
|
+
|
|
304
|
+
def _enter_value_expression(self, value, scope: BlockScope, pending: list) -> None:
|
|
305
|
+
"""An arm that is a block is a Pine local scope; any other child is in `scope`."""
|
|
306
|
+
headers = [value.condition] if isinstance(value, IfExpression) else (
|
|
307
|
+
[value.expr] + [case for case, _ in value.cases])
|
|
308
|
+
pending.extend((header, scope, value) for header in headers if header is not None)
|
|
309
|
+
for arm in _value_arms(value):
|
|
310
|
+
arm_scope = scope
|
|
311
|
+
if isinstance(arm, list):
|
|
312
|
+
arm_scope = self.block_scopes[id(arm)] = BlockScope(scope)
|
|
313
|
+
if arm is not None:
|
|
314
|
+
pending.append((arm, arm_scope, value))
|
|
315
|
+
|
|
316
|
+
def _bind(self, statement, scope: BlockScope) -> None:
|
|
317
|
+
if isinstance(statement, Reassignment):
|
|
318
|
+
if isinstance(statement.value, VALUE_NODES):
|
|
319
|
+
self.contexts.setdefault(id(statement.value), []).append((statement.target, scope))
|
|
320
|
+
return
|
|
321
|
+
if isinstance(statement, Assignment) and isinstance(statement.target, TupleDestructure):
|
|
322
|
+
for position, name in enumerate(statement.target.names):
|
|
323
|
+
element = Binding(None, None, scope)
|
|
324
|
+
self.tuple_elements[id(element)] = (element, statement.value, position)
|
|
325
|
+
scope.bind(name, element)
|
|
326
|
+
return
|
|
327
|
+
if isinstance(statement, InputDecl) and statement.func == 'input.bool':
|
|
328
|
+
scope.bind(statement.name, Binding('bool', None, scope))
|
|
329
|
+
return
|
|
330
|
+
bind_declaration(statement, scope)
|
|
331
|
+
if getattr(statement, 'type_hint', None) == 'bool' and isinstance(statement.value, VALUE_NODES):
|
|
332
|
+
self.contexts.setdefault(id(statement.value), []).append(True)
|
|
333
|
+
|
|
334
|
+
def _record_na_read(self, value, scope: BlockScope) -> None:
|
|
335
|
+
"""`x[n]` or `na(x)` of a name: where a v5 bool may be na."""
|
|
336
|
+
if isinstance(value, IndexAccess):
|
|
337
|
+
target = value.object
|
|
338
|
+
elif (isinstance(value.func, NaLiteral) or value.func == 'na') and len(value.args) == 1:
|
|
339
|
+
target = value.args[0]
|
|
340
|
+
else:
|
|
341
|
+
return
|
|
342
|
+
if isinstance(target, Identifier) and '.' not in target.name:
|
|
343
|
+
self.na_reads.append((target.name, scope))
|
|
344
|
+
if not isinstance(value, IndexAccess):
|
|
345
|
+
self.na_calls.append((target.name, scope)) # `na(x)` itself
|
|
346
|
+
|
|
347
|
+
# ------------------------------------------------------------ solving
|
|
348
|
+
def bool_keys(self, extra_roots: tuple = ()) -> set:
|
|
349
|
+
"""The keys whose answer is bool: the least fixpoint of their formulas."""
|
|
350
|
+
roots = [('function' if isinstance(node, FuncDecl) else 'value', node_id)
|
|
351
|
+
for node_id, node in list(self.nodes.items()) if not isinstance(node, Binding)]
|
|
352
|
+
formulas, dependents = self._formulas(roots + list(extra_roots))
|
|
353
|
+
for key, owners in self.implied_by.items():
|
|
354
|
+
for owner in owners:
|
|
355
|
+
dependents.setdefault(owner, []).append(key)
|
|
356
|
+
true_keys: set = set()
|
|
357
|
+
queue = list(formulas)
|
|
358
|
+
while queue:
|
|
359
|
+
key = queue.pop()
|
|
360
|
+
if key in true_keys:
|
|
361
|
+
continue
|
|
362
|
+
if any(owner in true_keys for owner in self.implied_by.get(key, ())) or _holds(formulas[key], true_keys):
|
|
363
|
+
true_keys.add(key)
|
|
364
|
+
queue.extend(dependents.get(key, ()))
|
|
365
|
+
return true_keys
|
|
366
|
+
|
|
367
|
+
def _formulas(self, roots: list) -> tuple:
|
|
368
|
+
formulas: Dict[Key, object] = {}
|
|
369
|
+
dependents: Dict[Key, List[Key]] = {}
|
|
370
|
+
pending = list(roots)
|
|
371
|
+
while pending:
|
|
372
|
+
key = pending.pop()
|
|
373
|
+
if key in formulas:
|
|
374
|
+
continue
|
|
375
|
+
formula = formulas[key] = self._formula_of(key)
|
|
376
|
+
for reference in _refs(formula):
|
|
377
|
+
dependents.setdefault(reference, []).append(key)
|
|
378
|
+
if reference not in formulas:
|
|
379
|
+
pending.append(reference)
|
|
380
|
+
return formulas, dependents
|
|
381
|
+
|
|
382
|
+
def _formula_of(self, key: Key):
|
|
383
|
+
kind, node_id = key
|
|
384
|
+
node = self.nodes[node_id]
|
|
385
|
+
if kind == 'binding':
|
|
386
|
+
return self._binding_formula(node)
|
|
387
|
+
if kind == 'function':
|
|
388
|
+
scope = self.function_scopes[node_id]
|
|
389
|
+
if isinstance(node.body, list):
|
|
390
|
+
return self._tail(node.body[-1], scope, key) if node.body else False
|
|
391
|
+
return self._tail_value(node.body, scope, key)
|
|
392
|
+
scope = self.scope_of[node_id]
|
|
393
|
+
contexts = [context if context is True else self._name(*context)
|
|
394
|
+
for context in self.contexts.get(node_id, ())]
|
|
395
|
+
return _any(contexts + [self._arm(arm, scope, key) for arm in _value_arms(node)])
|
|
396
|
+
|
|
397
|
+
# ------------------------------------------------------------ formulas
|
|
398
|
+
def _arm(self, arm, scope: BlockScope, owner: Key):
|
|
399
|
+
if not arm:
|
|
400
|
+
return False
|
|
401
|
+
if isinstance(arm, list):
|
|
402
|
+
return self._tail(arm[-1], self.block_scopes.get(id(arm), scope), owner)
|
|
403
|
+
return self._tail_value(arm, scope, owner)
|
|
404
|
+
|
|
405
|
+
def _tail(self, tail, scope: BlockScope, owner: Key):
|
|
406
|
+
"""The formula of the last statement of a block, whose value is the block's."""
|
|
407
|
+
if isinstance(tail, ExpressionStatement):
|
|
408
|
+
return self._tail_value(tail.expr, scope, owner)
|
|
409
|
+
if isinstance(tail, (IfStatement, SwitchStatement)):
|
|
410
|
+
return _any(self._arm(arm, scope, owner) for arm in _statement_arms(tail))
|
|
411
|
+
if isinstance(tail, BINDINGS):
|
|
412
|
+
target = tail.name if isinstance(tail, (VarDecl, VaripDecl)) else tail.target
|
|
413
|
+
return self._name(target, scope) if isinstance(target, str) else False
|
|
414
|
+
return self._tail_value(tail, scope, owner)
|
|
415
|
+
|
|
416
|
+
def _tail_value(self, value, scope: BlockScope, owner: Key):
|
|
417
|
+
"""A value in the tail of `owner` has its type: one if/switch has one type."""
|
|
418
|
+
if isinstance(value, VALUE_NODES):
|
|
419
|
+
key = ('value', id(value))
|
|
420
|
+
self.implied_by.setdefault(key, []).append(owner)
|
|
421
|
+
return ('ref', key)
|
|
422
|
+
return self._expression(value, scope)
|
|
423
|
+
|
|
424
|
+
def _expression(self, value, scope: BlockScope):
|
|
425
|
+
if isinstance(value, Literal):
|
|
426
|
+
return value.literal_type == 'bool'
|
|
427
|
+
if isinstance(value, BinaryOp):
|
|
428
|
+
return value.op in BOOL_OPERATORS
|
|
429
|
+
if isinstance(value, UnaryOp):
|
|
430
|
+
return value.op == 'not'
|
|
431
|
+
if isinstance(value, TernaryOp):
|
|
432
|
+
return _all((self._expression(value.true_expr, scope), self._expression(value.false_expr, scope)))
|
|
433
|
+
if isinstance(value, VALUE_NODES):
|
|
434
|
+
return ('ref', ('value', id(value)))
|
|
435
|
+
if isinstance(value, IndexAccess):
|
|
436
|
+
return self._expression(value.object, scope)
|
|
437
|
+
if isinstance(value, FunctionCall):
|
|
438
|
+
return self._call(value, scope)
|
|
439
|
+
if isinstance(value, Identifier) and '.' not in value.name:
|
|
440
|
+
return self._name(value.name, scope)
|
|
441
|
+
if isinstance(value, (Identifier, MemberAccess)):
|
|
442
|
+
path = receiver_path(value)
|
|
443
|
+
return ((path is not None and '.'.join(path) in BOOL_BUILTINS)
|
|
444
|
+
or self.resolver.field_type_in_scope(value, scope) == 'bool')
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
def _name(self, name: str, scope: BlockScope):
|
|
448
|
+
if name in ('true', 'false'):
|
|
449
|
+
return True
|
|
450
|
+
binding = scope.lookup(name)
|
|
451
|
+
if binding is None:
|
|
452
|
+
return False
|
|
453
|
+
if binding.initialiser is None and id(binding) not in self.tuple_elements:
|
|
454
|
+
return binding.declared_type == 'bool'
|
|
455
|
+
self.nodes[id(binding)] = binding
|
|
456
|
+
return ('ref', ('binding', id(binding)))
|
|
457
|
+
|
|
458
|
+
def _binding_formula(self, binding: Binding):
|
|
459
|
+
if id(binding) in self.tuple_elements:
|
|
460
|
+
_, value, position = self.tuple_elements[id(binding)]
|
|
461
|
+
return self._tuple_element(value, position)
|
|
462
|
+
return self._expression(binding.initialiser, binding.scope)
|
|
463
|
+
|
|
464
|
+
def _tuple_element(self, value, position: int):
|
|
465
|
+
"""`[p, q] = f()`: p is bool when every f's tail tuple has a bool at p."""
|
|
466
|
+
if not (isinstance(value, FunctionCall) and isinstance(value.func, str) and value.func in self.functions):
|
|
467
|
+
return False
|
|
468
|
+
return _all(self._tail_element(function, position) for function in self.functions[value.func])
|
|
469
|
+
|
|
470
|
+
def _tail_element(self, function: FuncDecl, position: int):
|
|
471
|
+
body = function.body
|
|
472
|
+
tail = body[-1].expr if isinstance(body, list) and body and isinstance(body[-1], ExpressionStatement) else body
|
|
473
|
+
elements = getattr(tail, 'elements', None)
|
|
474
|
+
if not elements or position >= len(elements):
|
|
475
|
+
return False
|
|
476
|
+
return self._expression(elements[position], self.function_scopes[id(function)])
|
|
477
|
+
|
|
478
|
+
# ------------------------------------------------------------ calls
|
|
479
|
+
def _call(self, call: FunctionCall, scope: BlockScope):
|
|
480
|
+
name = 'na' if isinstance(call.func, NaLiteral) else call.func
|
|
481
|
+
if not isinstance(name, str):
|
|
482
|
+
return False
|
|
483
|
+
if name in self.functions:
|
|
484
|
+
return self._user_call(self.functions[name], call.args, scope)
|
|
485
|
+
if name in BOOL_BUILTINS or name in BOOL_CONSTRUCTS:
|
|
486
|
+
return True
|
|
487
|
+
receiver, dot, method = name.rpartition('.')
|
|
488
|
+
if not dot:
|
|
489
|
+
return False
|
|
490
|
+
if receiver in NAMESPACE_MEMBERS or receiver in RECEIVER_NAMESPACES.values():
|
|
491
|
+
# `array.get(a, 0)`: the function form of a method on its first argument
|
|
492
|
+
is_element_access = receiver == 'array' and method in ARRAY_ELEMENT_METHODS and call.args
|
|
493
|
+
return self._method_on(self._container(call.args[0], scope), method) if is_element_access else False
|
|
494
|
+
methods = [function for function in self.functions.get(method, ()) if function.is_method]
|
|
495
|
+
if methods:
|
|
496
|
+
return self._user_call(methods, [Identifier(name=receiver)] + list(call.args), scope)
|
|
497
|
+
return self._method_on(self._container(Identifier(name=receiver), scope), method)
|
|
498
|
+
|
|
499
|
+
def _user_call(self, declarations: list, arguments: list, scope: BlockScope):
|
|
500
|
+
if len(declarations) > 1:
|
|
501
|
+
declarations = overloads_taking(declarations, [self._argument_type(argument, scope)
|
|
502
|
+
for argument in arguments])
|
|
503
|
+
return _all(('ref', ('function', id(function))) for function in declarations) if declarations else False
|
|
504
|
+
|
|
505
|
+
def _argument_type(self, argument, scope: BlockScope) -> Optional[str]:
|
|
506
|
+
if isinstance(argument, Identifier) and scope.lookup(argument.name) is None:
|
|
507
|
+
return BUILTIN_SERIES_TYPES.get(argument.name)
|
|
508
|
+
return self.resolver.scalar_type(argument, scope)
|
|
509
|
+
|
|
510
|
+
@staticmethod
|
|
511
|
+
def _method_on(container, method: str):
|
|
512
|
+
"""A method of a known builtin receiver: bool by its builtin, or an `array<bool>` element."""
|
|
513
|
+
if container is None:
|
|
514
|
+
return False
|
|
515
|
+
kind, element_is_bool = container
|
|
516
|
+
if f'{RECEIVER_NAMESPACES[kind]}.{method}' in BOOL_BUILTINS:
|
|
517
|
+
return True
|
|
518
|
+
return element_is_bool if kind == 'array' and method in ARRAY_ELEMENT_METHODS else False
|
|
519
|
+
|
|
520
|
+
def _container(self, expression, scope: BlockScope):
|
|
521
|
+
"""(kind, element formula) when `expression` is a builtin array, map,
|
|
522
|
+
matrix or string: declared, constructed, or bound to one; else None."""
|
|
523
|
+
followed = set()
|
|
524
|
+
while True:
|
|
525
|
+
if isinstance(expression, Literal):
|
|
526
|
+
return ('string', False) if expression.literal_type == 'string' else None
|
|
527
|
+
if isinstance(expression, FunctionCall):
|
|
528
|
+
return self._constructed_container(expression, scope)
|
|
529
|
+
if isinstance(expression, Identifier) and '.' not in expression.name:
|
|
530
|
+
binding = scope.lookup(expression.name)
|
|
531
|
+
if binding is None or id(binding) in followed:
|
|
532
|
+
return None
|
|
533
|
+
if binding.initialiser is None:
|
|
534
|
+
return _declared_container(binding.declared_type)
|
|
535
|
+
followed.add(id(binding))
|
|
536
|
+
expression, scope = binding.initialiser, binding.scope
|
|
537
|
+
continue
|
|
538
|
+
if isinstance(expression, (Identifier, MemberAccess)):
|
|
539
|
+
return _declared_container(self.resolver.field_type_in_scope(expression, scope))
|
|
540
|
+
return None
|
|
541
|
+
|
|
542
|
+
def _constructed_container(self, call: FunctionCall, scope: BlockScope):
|
|
543
|
+
func = call.func if isinstance(call.func, str) else ''
|
|
544
|
+
if func == 'array.from':
|
|
545
|
+
return 'array', (_all(self._expression(argument, scope) for argument in call.args)
|
|
546
|
+
if call.args else False)
|
|
547
|
+
if func.startswith('array.new'):
|
|
548
|
+
return 'array', func in ('array.new_bool', 'array.new<bool>')
|
|
549
|
+
kind = func.partition('.new')[0] if '.new' in func else None
|
|
550
|
+
return (kind, False) if kind in ('map', 'matrix') else None
|
|
551
|
+
|
|
552
|
+
# ------------------------------------------------------------ restamping
|
|
553
|
+
def ancestors_of(self, node_ids: set) -> set:
|
|
554
|
+
"""The ids of every node above one of `node_ids`."""
|
|
555
|
+
ancestors: set = set()
|
|
556
|
+
pending = list(node_ids)
|
|
557
|
+
while pending:
|
|
558
|
+
for parent in self.parents.get(pending.pop(), ()):
|
|
559
|
+
if parent is not None and id(parent) not in ancestors:
|
|
560
|
+
ancestors.add(id(parent))
|
|
561
|
+
pending.append(id(parent))
|
|
562
|
+
return ancestors
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
@dataclasses.dataclass(frozen=True)
|
|
566
|
+
class _Restamp:
|
|
567
|
+
"""Copies only the nodes on the way to a stamped one; the rest is shared."""
|
|
568
|
+
stamped: set
|
|
569
|
+
ancestors: set
|
|
570
|
+
|
|
571
|
+
def rebuilt(self, node):
|
|
572
|
+
node_id = id(node)
|
|
573
|
+
if node_id not in self.ancestors and node_id not in self.stamped:
|
|
574
|
+
return node
|
|
575
|
+
changes = {}
|
|
576
|
+
if node_id in self.ancestors:
|
|
577
|
+
changes = {name: map_children(getattr(node, name), self.rebuilt) for name in child_field_names(type(node))}
|
|
578
|
+
if node_id in self.stamped:
|
|
579
|
+
changes['value_is_bool'] = True
|
|
580
|
+
return dataclasses.replace(node, **changes)
|