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,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)