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,475 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Overload resolution for same-name Pine functions and methods (card #39).
|
|
3
|
+
|
|
4
|
+
Pine picks an overload from the static types of ALL its arguments. pynecore's
|
|
5
|
+
``@overload`` selects the same way from every parameter's annotation (user
|
|
6
|
+
types, drawings, containers, typed na, int before float), so each same-name
|
|
7
|
+
declaration keeps its name and is emitted under ``@overload``; no dispatcher
|
|
8
|
+
is generated. Two declarations the runtime cannot tell apart are refused.
|
|
9
|
+
|
|
10
|
+
pynecore selects by the VALUE it is given, Pine by the declared TYPE. They
|
|
11
|
+
disagree only where one overload takes an int and another a float, and a
|
|
12
|
+
float-typed value holds a Python int (``float v = 1``). At such a slot an
|
|
13
|
+
argument whose float type comes from declarations is passed as ``float(...)``.
|
|
14
|
+
Declarations are: a typed name or parameter, ``input.float`` / ``input.int``,
|
|
15
|
+
a literal, an untyped name whose initialiser is itself declared (followed in
|
|
16
|
+
the scope that owns the initialiser), a history read or UDT field of such a
|
|
17
|
+
value, a sign, and ``+ - * %`` or a ternary / if / switch over such values.
|
|
18
|
+
Expression inference (``/``, ``ta.*``, other calls) is never used: it calls
|
|
19
|
+
int-valued Pine expressions float, and pynecore's static type pin types them.
|
|
20
|
+
|
|
21
|
+
Scopes follow Pine: the global scope, one per function and one per nested
|
|
22
|
+
block. ``rename_shadowing_locals`` (card #38) runs first, so a name bound in an
|
|
23
|
+
inner scope never hides an outer binding of the same name here. Within ONE
|
|
24
|
+
scope, a second binding with a different type or initialiser makes the name
|
|
25
|
+
unknown.
|
|
26
|
+
"""
|
|
27
|
+
import dataclasses
|
|
28
|
+
import re
|
|
29
|
+
from typing import Callable, Dict, FrozenSet, List, NamedTuple, Optional, Tuple
|
|
30
|
+
|
|
31
|
+
from .ast_nodes import (
|
|
32
|
+
Assignment, BinaryOp, Expression, ExpressionStatement, ForInLoop, ForLoop, FuncDecl,
|
|
33
|
+
FunctionCall, Identifier, IfExpression, IfStatement, IndexAccess, InputDecl, Literal,
|
|
34
|
+
MemberAccess, MethodCall, Script, SwitchExpression, SwitchStatement, TernaryOp,
|
|
35
|
+
TupleDestructure, UnaryOp, VarDecl, VaripDecl, WhileLoop,
|
|
36
|
+
)
|
|
37
|
+
from .errors import UnsupportedFeatureError
|
|
38
|
+
from .type_inference import receiver_path
|
|
39
|
+
|
|
40
|
+
SCALAR_TYPES = ('int', 'float')
|
|
41
|
+
SIGN_OPERATORS = ('+', '-')
|
|
42
|
+
# Pine's numeric promotion: int op int is int, any float operand makes it float.
|
|
43
|
+
# `/` is left out: Pine v6 gives a fractional result for int / int.
|
|
44
|
+
PROMOTING_OPERATORS = ('+', '-', '*', '%')
|
|
45
|
+
INPUT_TYPES = {'input.float': 'float', 'input.int': 'int'}
|
|
46
|
+
# Untyped names followed per argument before its type is given up as unknown
|
|
47
|
+
MAXIMUM_CHAIN_LINKS = 256
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ============================================================================
|
|
51
|
+
# @overload declarations
|
|
52
|
+
# ============================================================================
|
|
53
|
+
|
|
54
|
+
def mark_overloaded_declarations(main_body: list, enum_names: set,
|
|
55
|
+
runtime_type_name: Callable[[Optional[str]], Optional[str]]) -> Tuple[list, bool]:
|
|
56
|
+
"""Return `main_body` with every same-name function declaration marked
|
|
57
|
+
``is_overload`` (emitted under ``@overload``), and whether there is one.
|
|
58
|
+
|
|
59
|
+
An enum parameter keeps its enum's name, as PyneComp writes it.
|
|
60
|
+
"""
|
|
61
|
+
declarations_by_name: Dict[str, List[FuncDecl]] = {}
|
|
62
|
+
for statement in main_body:
|
|
63
|
+
if isinstance(statement, FuncDecl):
|
|
64
|
+
declarations_by_name.setdefault(statement.name, []).append(statement)
|
|
65
|
+
overloaded_groups = [group for group in declarations_by_name.values() if len(group) > 1]
|
|
66
|
+
for group in overloaded_groups:
|
|
67
|
+
refuse_indistinguishable_overloads(group, enum_names, runtime_type_name)
|
|
68
|
+
overloaded = {id(declaration) for group in overloaded_groups for declaration in group}
|
|
69
|
+
marked_body = [_as_overload(statement, enum_names) if id(statement) in overloaded else statement
|
|
70
|
+
for statement in main_body]
|
|
71
|
+
return marked_body, bool(overloaded)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def with_enums_as_str(type_hint: Optional[str], enum_names: set) -> Optional[str]:
|
|
75
|
+
"""The annotation the runtime sees: every enum name, alone or inside a
|
|
76
|
+
container (`list[E]`, `Matrix[E]`, `dict[str, E]`), becomes ``str``."""
|
|
77
|
+
if not type_hint or not enum_names:
|
|
78
|
+
return type_hint
|
|
79
|
+
enum_pattern = r'\b(?:' + '|'.join(re.escape(name) for name in sorted(enum_names)) + r')\b'
|
|
80
|
+
return re.sub(enum_pattern, 'str', type_hint)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _as_overload(declaration: FuncDecl, enum_names: set) -> FuncDecl:
|
|
84
|
+
# an enum parameter keeps its enum's name, as PyneComp writes it (chrono_utils `timezone: Timezone`)
|
|
85
|
+
return dataclasses.replace(declaration, is_overload=True)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def refuse_indistinguishable_overloads(declarations: List[FuncDecl], enum_names: set,
|
|
89
|
+
runtime_type_name: Callable[[Optional[str]], Optional[str]]) -> None:
|
|
90
|
+
"""Two overloads with the same runtime parameter types (`simple string` and
|
|
91
|
+
`string`, `f(E e)` next to `f(string s)`) are kept, as PyneComp keeps them;
|
|
92
|
+
pynecore runs the first that matches."""
|
|
93
|
+
earlier_by_runtime_types: Dict[tuple, FuncDecl] = {}
|
|
94
|
+
for declaration in declarations:
|
|
95
|
+
runtime_types = tuple(runtime_type_name(with_enums_as_str(parameter.type_hint, enum_names))
|
|
96
|
+
for parameter in declaration.params)
|
|
97
|
+
earlier_by_runtime_types.setdefault(runtime_types, declaration)
|
|
98
|
+
# PyneComp emits such overloads as written (`simple string this` next to `string this`, the
|
|
99
|
+
# reference's jason5480 libraries); pynecore runs the first that matches, as the Pine call would
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _collision_message(earlier: FuncDecl, later: FuncDecl, runtime_types: tuple, enum_names: set) -> str:
|
|
103
|
+
def label(type_hint):
|
|
104
|
+
if type_hint in enum_names:
|
|
105
|
+
return f'enum {type_hint}'
|
|
106
|
+
enums_inside = sorted(name for name in enum_names if re.search(rf'\b{re.escape(name)}\b', str(type_hint)))
|
|
107
|
+
return f"{type_hint} (enum {', '.join(enums_inside)})" if enums_inside else str(type_hint)
|
|
108
|
+
differences = [f'{label(first.type_hint)} (line {earlier.line}) and {label(second.type_hint)} (line {later.line})'
|
|
109
|
+
for first, second in zip(earlier.params, later.params) if first.type_hint != second.type_hint]
|
|
110
|
+
collision = ('; '.join(differences) + ' are the same type at runtime' if differences
|
|
111
|
+
else 'both declare the same parameter types')
|
|
112
|
+
return (f"overloads of '{later.name}' on lines {earlier.line} and {later.line} cannot be told apart: "
|
|
113
|
+
f"{collision}, {runtime_types}, so a call cannot pick one")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ============================================================================
|
|
117
|
+
# Slots where one overload takes an int and another a float
|
|
118
|
+
# ============================================================================
|
|
119
|
+
|
|
120
|
+
class IntFloatSlots(NamedTuple):
|
|
121
|
+
"""The positions and parameter names at which one overload of a name takes
|
|
122
|
+
an int and another a float."""
|
|
123
|
+
positions: FrozenSet[int]
|
|
124
|
+
parameter_names: FrozenSet[str]
|
|
125
|
+
|
|
126
|
+
def includes(self, slot) -> bool:
|
|
127
|
+
return slot in (self.positions if isinstance(slot, int) else self.parameter_names)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def int_float_overload_slots(declarations: list) -> Dict[str, IntFloatSlots]:
|
|
131
|
+
"""Overloaded Pine name -> its int/float slots, for the names that have any."""
|
|
132
|
+
declarations_by_name: Dict[str, List[FuncDecl]] = {}
|
|
133
|
+
for declaration in declarations:
|
|
134
|
+
if isinstance(declaration, FuncDecl):
|
|
135
|
+
declarations_by_name.setdefault(declaration.name, []).append(declaration)
|
|
136
|
+
slots_by_name: Dict[str, IntFloatSlots] = {}
|
|
137
|
+
for name, group in declarations_by_name.items():
|
|
138
|
+
if len(group) < 2:
|
|
139
|
+
continue
|
|
140
|
+
types_by_position: Dict[int, set] = {}
|
|
141
|
+
types_by_parameter_name: Dict[str, set] = {}
|
|
142
|
+
for declaration in group:
|
|
143
|
+
for position, parameter in enumerate(declaration.params):
|
|
144
|
+
types_by_position.setdefault(position, set()).add(parameter.type_hint)
|
|
145
|
+
types_by_parameter_name.setdefault(parameter.name, set()).add(parameter.type_hint)
|
|
146
|
+
slots = IntFloatSlots(_int_and_float(types_by_position), _int_and_float(types_by_parameter_name))
|
|
147
|
+
if slots.positions or slots.parameter_names:
|
|
148
|
+
slots_by_name[name] = slots
|
|
149
|
+
return slots_by_name
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _int_and_float(types_by_slot: dict) -> frozenset:
|
|
153
|
+
return frozenset(slot for slot, types in types_by_slot.items() if set(SCALAR_TYPES) <= types)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def overloads_taking(declarations: List[FuncDecl], argument_types: List[Optional[str]]) -> List[FuncDecl]:
|
|
157
|
+
"""The overloads of one name a call with these argument types can select,
|
|
158
|
+
as Pine resolves them (card #56): those the call can reach (a parameter
|
|
159
|
+
for every argument, an argument for every parameter without a default),
|
|
160
|
+
then those whose typed parameters take each argument's type exactly, else
|
|
161
|
+
those that take it once an int is promoted to float. An argument of
|
|
162
|
+
unknown type (None) and an untyped parameter rule nothing out, so every
|
|
163
|
+
overload of the result may be the one that runs."""
|
|
164
|
+
def takes(declaration: FuncDecl, promoting: bool) -> bool:
|
|
165
|
+
return all(parameter.type_hint is None or argument_type is None or parameter.type_hint == argument_type
|
|
166
|
+
or (promoting and (argument_type, parameter.type_hint) == ('int', 'float'))
|
|
167
|
+
for parameter, argument_type in zip(declaration.params, argument_types))
|
|
168
|
+
reachable = [declaration for declaration in declarations
|
|
169
|
+
if sum(parameter.default is None for parameter in declaration.params)
|
|
170
|
+
<= len(argument_types) <= len(declaration.params)]
|
|
171
|
+
exact = [declaration for declaration in reachable if takes(declaration, promoting=False)]
|
|
172
|
+
return exact or [declaration for declaration in reachable if takes(declaration, promoting=True)]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ============================================================================
|
|
176
|
+
# Scopes and the declarations they hold
|
|
177
|
+
# ============================================================================
|
|
178
|
+
|
|
179
|
+
class Binding(NamedTuple):
|
|
180
|
+
"""What one declaration says about a name: its declared Pine type, or the
|
|
181
|
+
initialiser of an untyped declaration together with the scope owning it."""
|
|
182
|
+
declared_type: Optional[str]
|
|
183
|
+
initialiser: Optional[Expression]
|
|
184
|
+
scope: Optional['BlockScope']
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
UNKNOWN_BINDING = Binding(None, None, None)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class BlockScope:
|
|
191
|
+
"""One Pine scope: the global scope, a function body, or a nested block."""
|
|
192
|
+
|
|
193
|
+
def __init__(self, parent: Optional['BlockScope'] = None):
|
|
194
|
+
self.parent = parent
|
|
195
|
+
self.bindings: Dict[str, Binding] = {}
|
|
196
|
+
|
|
197
|
+
def bind(self, name: str, binding: Binding) -> None:
|
|
198
|
+
"""A second binding in this scope that says something else makes the name unknown."""
|
|
199
|
+
earlier = self.bindings.get(name)
|
|
200
|
+
agrees = (earlier is not None and earlier.declared_type == binding.declared_type
|
|
201
|
+
and earlier.initialiser is binding.initialiser)
|
|
202
|
+
self.bindings[name] = binding if earlier is None or agrees else UNKNOWN_BINDING
|
|
203
|
+
|
|
204
|
+
def lookup(self, name: str) -> Optional[Binding]:
|
|
205
|
+
scope = self
|
|
206
|
+
while scope is not None:
|
|
207
|
+
if name in scope.bindings:
|
|
208
|
+
return scope.bindings[name]
|
|
209
|
+
scope = scope.parent
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def collect_call_scopes(script: Script, visit_ast_nodes) -> Dict[int, BlockScope]:
|
|
214
|
+
"""id() of every function and method call in `script` -> the scope it is in,
|
|
215
|
+
with every scope's declarations bound. `visit_ast_nodes` is the
|
|
216
|
+
transformer's depth-first walker."""
|
|
217
|
+
call_scopes: Dict[int, BlockScope] = {}
|
|
218
|
+
|
|
219
|
+
def note_calls(nodes: list, scope: BlockScope) -> None:
|
|
220
|
+
def note(node):
|
|
221
|
+
if isinstance(node, (FunctionCall, MethodCall)):
|
|
222
|
+
call_scopes[id(node)] = scope
|
|
223
|
+
# The walker enters the (condition, body) pairs of else-if arms and
|
|
224
|
+
# switch cases (ast_walk, card #32); walking them again here would
|
|
225
|
+
# revisit each nested branch once per ancestor
|
|
226
|
+
visit_ast_nodes(nodes, note)
|
|
227
|
+
|
|
228
|
+
def walk_block(statements: list, scope: BlockScope) -> None:
|
|
229
|
+
# Every call in a statement is first given the statement's scope; the
|
|
230
|
+
# calls inside each nested block are then given that block's scope
|
|
231
|
+
for statement in statements:
|
|
232
|
+
bind_declaration(statement, scope)
|
|
233
|
+
note_calls([statement], scope)
|
|
234
|
+
for body, block_scope in nested_blocks(statement, scope):
|
|
235
|
+
walk_block(body, block_scope)
|
|
236
|
+
|
|
237
|
+
walk_block(list(script.declarations) + list(script.body), BlockScope())
|
|
238
|
+
return call_scopes
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def bind_declaration(statement, scope: BlockScope) -> None:
|
|
242
|
+
"""Bind the name(s) a statement declares in `scope`."""
|
|
243
|
+
if isinstance(statement, Assignment) and isinstance(statement.target, TupleDestructure):
|
|
244
|
+
for name in statement.target.names:
|
|
245
|
+
scope.bind(name, UNKNOWN_BINDING)
|
|
246
|
+
elif isinstance(statement, (Assignment, VarDecl, VaripDecl)):
|
|
247
|
+
name = statement.target if isinstance(statement, Assignment) else statement.name
|
|
248
|
+
if statement.type_hint:
|
|
249
|
+
scope.bind(name, Binding(statement.type_hint, None, scope))
|
|
250
|
+
else:
|
|
251
|
+
scope.bind(name, Binding(None, statement.value, scope))
|
|
252
|
+
elif isinstance(statement, InputDecl):
|
|
253
|
+
scope.bind(statement.name, Binding(INPUT_TYPES.get(statement.func), None, scope))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def nested_blocks(statement, scope: BlockScope) -> List[Tuple[list, BlockScope]]:
|
|
257
|
+
"""The blocks a statement opens, each with its own scope under `scope`."""
|
|
258
|
+
def opened(body, bound_names=(), parameters=()) -> Tuple[list, BlockScope]:
|
|
259
|
+
block_scope = BlockScope(scope)
|
|
260
|
+
for name in bound_names:
|
|
261
|
+
block_scope.bind(name, UNKNOWN_BINDING)
|
|
262
|
+
for parameter in parameters:
|
|
263
|
+
block_scope.bind(parameter.name, Binding(parameter.type_hint, None, block_scope))
|
|
264
|
+
return (body if isinstance(body, list) else [body]), block_scope
|
|
265
|
+
|
|
266
|
+
if isinstance(statement, FuncDecl):
|
|
267
|
+
return [opened(statement.body, parameters=statement.params)]
|
|
268
|
+
if isinstance(statement, IfStatement):
|
|
269
|
+
bodies = [statement.body] + [body for _, body in statement.elseifs] + [statement.else_body]
|
|
270
|
+
elif isinstance(statement, SwitchStatement):
|
|
271
|
+
bodies = [body for _, body in statement.cases] + [statement.default]
|
|
272
|
+
elif isinstance(statement, ForLoop):
|
|
273
|
+
return [opened(statement.body, bound_names=[statement.var])]
|
|
274
|
+
elif isinstance(statement, ForInLoop):
|
|
275
|
+
return [opened(statement.body, bound_names=statement.vars)]
|
|
276
|
+
elif isinstance(statement, WhileLoop):
|
|
277
|
+
bodies = [statement.body]
|
|
278
|
+
else:
|
|
279
|
+
return []
|
|
280
|
+
return [opened(body) for body in bodies if body]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ============================================================================
|
|
284
|
+
# Declared scalar types
|
|
285
|
+
# ============================================================================
|
|
286
|
+
|
|
287
|
+
class _Pending(NamedTuple):
|
|
288
|
+
"""One node on the explicit stack of DeclaredTypeResolver.scalar_type."""
|
|
289
|
+
node: object
|
|
290
|
+
scope: BlockScope
|
|
291
|
+
in_initialiser: bool = False # reached through a binding, not the call's own argument
|
|
292
|
+
operands: Optional[list] = None # [(node, scope)] once expanded
|
|
293
|
+
binding: Optional[Binding] = None # the binding an identifier was followed through
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class DeclaredTypeResolver:
|
|
297
|
+
"""'int' or 'float' for an expression whose type comes from declarations
|
|
298
|
+
alone, else None.
|
|
299
|
+
|
|
300
|
+
A UDT field path (`t.v`) written as the call's own argument is typed by
|
|
301
|
+
`field_type`, the transformer's receiver typing where the call is. One
|
|
302
|
+
inside an initialiser is typed from the scope that owns the initialiser,
|
|
303
|
+
with `udt_field_types` (type name -> field -> Pine type): the call's
|
|
304
|
+
function context (a parameter `t`) must not leak into a remembered type.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
def __init__(self, field_type: Callable[[Expression], Optional[str]], udt_field_types: Dict[str, Dict[str, str]]):
|
|
308
|
+
self.field_type = field_type
|
|
309
|
+
self.udt_field_types = udt_field_types
|
|
310
|
+
self.binding_types: Dict[int, Optional[str]] = {} # id(binding) -> type, once computed
|
|
311
|
+
|
|
312
|
+
def scalar_type(self, expression, scope: BlockScope) -> Optional[str]:
|
|
313
|
+
"""Iterative, so a long chain of untyped names (`x0 = x1`, `x1 = x2`, ...)
|
|
314
|
+
cannot exhaust the Python stack. Each binding is typed once and
|
|
315
|
+
remembered; the walk gives up (None) after MAXIMUM_CHAIN_LINKS links,
|
|
316
|
+
which also ends a cycle."""
|
|
317
|
+
types: Dict[int, Optional[str]] = {}
|
|
318
|
+
stack = [_Pending(expression, scope)]
|
|
319
|
+
links_followed = 0
|
|
320
|
+
while stack:
|
|
321
|
+
pending = stack.pop()
|
|
322
|
+
if pending.operands is not None:
|
|
323
|
+
types[id(pending.node)] = self._combined_type(pending, types)
|
|
324
|
+
continue
|
|
325
|
+
leaf_type, operands, binding = self._operands(pending.node, pending.scope, pending.in_initialiser)
|
|
326
|
+
if operands is None:
|
|
327
|
+
types[id(pending.node)] = leaf_type
|
|
328
|
+
continue
|
|
329
|
+
if binding is not None:
|
|
330
|
+
links_followed += 1
|
|
331
|
+
if links_followed > MAXIMUM_CHAIN_LINKS:
|
|
332
|
+
return None
|
|
333
|
+
stack.append(pending._replace(operands=operands, binding=binding))
|
|
334
|
+
in_initialiser = pending.in_initialiser or binding is not None
|
|
335
|
+
stack.extend(_Pending(node, operand_scope, in_initialiser) for node, operand_scope in operands)
|
|
336
|
+
return types[id(expression)]
|
|
337
|
+
|
|
338
|
+
def _operands(self, node, scope: BlockScope, in_initialiser: bool = False) -> tuple:
|
|
339
|
+
"""(type, None, None) for a leaf; (None, [(operand, scope)], binding) otherwise."""
|
|
340
|
+
leaf = (None, None, None)
|
|
341
|
+
if isinstance(node, Literal):
|
|
342
|
+
return _scalar(node.literal_type), None, None
|
|
343
|
+
if isinstance(node, UnaryOp):
|
|
344
|
+
return (None, [(node.operand, scope)], None) if node.op in SIGN_OPERATORS else leaf
|
|
345
|
+
if isinstance(node, BinaryOp):
|
|
346
|
+
return (None, [(node.left, scope), (node.right, scope)], None) if node.op in PROMOTING_OPERATORS else leaf
|
|
347
|
+
if isinstance(node, (TernaryOp, IfExpression, SwitchExpression)):
|
|
348
|
+
branch_values = _branch_values(node)
|
|
349
|
+
return leaf if None in branch_values else (None, [(value, scope) for value in branch_values], None)
|
|
350
|
+
if isinstance(node, IndexAccess):
|
|
351
|
+
return None, [(node.object, scope)], None
|
|
352
|
+
if isinstance(node, MemberAccess) or (isinstance(node, Identifier) and '.' in node.name):
|
|
353
|
+
field_type = self.field_type_in_scope(node, scope) if in_initialiser else self.field_type(node)
|
|
354
|
+
return _scalar(field_type), None, None
|
|
355
|
+
if isinstance(node, Identifier):
|
|
356
|
+
return self._identifier_operands(node.name, scope)
|
|
357
|
+
return leaf
|
|
358
|
+
|
|
359
|
+
def _identifier_operands(self, name: str, scope: BlockScope) -> tuple:
|
|
360
|
+
binding = scope.lookup(name)
|
|
361
|
+
if binding is None or binding.initialiser is None:
|
|
362
|
+
return (_scalar(binding.declared_type) if binding else None), None, None
|
|
363
|
+
if id(binding) in self.binding_types:
|
|
364
|
+
return self.binding_types[id(binding)], None, None
|
|
365
|
+
# The initialiser is typed in the scope that owns it, not at the call
|
|
366
|
+
return None, [(binding.initialiser, binding.scope)], binding
|
|
367
|
+
|
|
368
|
+
def field_type_in_scope(self, field_path, scope: BlockScope) -> Optional[str]:
|
|
369
|
+
"""The declared type of `t.v` / `t.inner.v` read in `scope`: the root
|
|
370
|
+
name's declared type, or the UDT its untyped initialiser constructs
|
|
371
|
+
(`t = T.new(...)`, `t = w.inner`), then each field's declared type."""
|
|
372
|
+
pending_fields: List[str] = []
|
|
373
|
+
expression, expression_scope = field_path, scope
|
|
374
|
+
for _ in range(MAXIMUM_CHAIN_LINKS):
|
|
375
|
+
path = receiver_path(expression)
|
|
376
|
+
if not path:
|
|
377
|
+
type_name = _constructed_type(expression, self.udt_field_types)
|
|
378
|
+
break
|
|
379
|
+
root, *fields = path
|
|
380
|
+
pending_fields = fields + pending_fields
|
|
381
|
+
binding = expression_scope.lookup(root)
|
|
382
|
+
if binding is None or binding.initialiser is None:
|
|
383
|
+
type_name = binding.declared_type if binding else None
|
|
384
|
+
break
|
|
385
|
+
expression, expression_scope = binding.initialiser, binding.scope
|
|
386
|
+
else:
|
|
387
|
+
return None
|
|
388
|
+
for field_name in pending_fields:
|
|
389
|
+
type_name = self.udt_field_types.get(type_name, {}).get(field_name)
|
|
390
|
+
return type_name
|
|
391
|
+
|
|
392
|
+
def _combined_type(self, pending: _Pending, types: dict) -> Optional[str]:
|
|
393
|
+
operand_types = [types[id(node)] for node, _ in pending.operands]
|
|
394
|
+
if isinstance(pending.node, (UnaryOp, IndexAccess, Identifier)):
|
|
395
|
+
combined = operand_types[0]
|
|
396
|
+
else:
|
|
397
|
+
combined = _promoted(operand_types)
|
|
398
|
+
if pending.binding is not None:
|
|
399
|
+
self.binding_types[id(pending.binding)] = combined
|
|
400
|
+
return combined
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _constructed_type(expression, udt_field_types: dict) -> Optional[str]:
|
|
404
|
+
"""'T' for `T.new(...)` when T is a user type, else None."""
|
|
405
|
+
if isinstance(expression, FunctionCall) and isinstance(expression.func, str) and expression.func.endswith('.new'):
|
|
406
|
+
type_name = expression.func[:-len('.new')]
|
|
407
|
+
return type_name if type_name in udt_field_types else None
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _scalar(type_name: Optional[str]) -> Optional[str]:
|
|
412
|
+
return type_name if type_name in SCALAR_TYPES else None
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _promoted(operand_types: list) -> Optional[str]:
|
|
416
|
+
"""Pine's numeric promotion: float if any operand is float, int if all are int."""
|
|
417
|
+
if not operand_types or not all(type_name in SCALAR_TYPES for type_name in operand_types):
|
|
418
|
+
return None
|
|
419
|
+
return 'float' if 'float' in operand_types else 'int'
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _branch_values(node) -> list:
|
|
423
|
+
"""The value expression of every branch; None for a branch with no single value."""
|
|
424
|
+
if isinstance(node, TernaryOp):
|
|
425
|
+
return [node.true_expr, node.false_expr]
|
|
426
|
+
if isinstance(node, IfExpression):
|
|
427
|
+
branches = [node.true_expr] + ([node.false_expr] if node.false_expr is not None else [])
|
|
428
|
+
else:
|
|
429
|
+
branches = [body for _, body in node.cases] + ([node.default] if node.default is not None else [])
|
|
430
|
+
return [_branch_value(branch) for branch in branches]
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _branch_value(branch) -> Optional[Expression]:
|
|
434
|
+
last = branch[-1] if isinstance(branch, list) and branch else branch
|
|
435
|
+
if isinstance(last, ExpressionStatement):
|
|
436
|
+
return last.expr
|
|
437
|
+
return last if isinstance(last, Expression) else None
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# ============================================================================
|
|
441
|
+
# Coercion at the call site
|
|
442
|
+
# ============================================================================
|
|
443
|
+
|
|
444
|
+
class OverloadArgumentCoercion:
|
|
445
|
+
"""Passes a float-typed argument as ``float(...)`` at the int/float slots
|
|
446
|
+
of an overloaded name, so pynecore selects the overload Pine does."""
|
|
447
|
+
|
|
448
|
+
def __init__(self, script: Script, visit_ast_nodes, field_type: Callable[[Expression], Optional[str]],
|
|
449
|
+
udt_field_types: Dict[str, Dict[str, str]]):
|
|
450
|
+
self.slots_by_name = int_float_overload_slots(script.declarations)
|
|
451
|
+
self.call_scopes = collect_call_scopes(script, visit_ast_nodes) if self.slots_by_name else {}
|
|
452
|
+
self.resolver = DeclaredTypeResolver(field_type, udt_field_types)
|
|
453
|
+
|
|
454
|
+
def coerce(self, call, pine_name, pine_arguments: list, arguments: list,
|
|
455
|
+
pine_keywords: dict, keywords: dict) -> Tuple[list, dict]:
|
|
456
|
+
"""`call` is the Pine call node; the pine_* values are its untransformed
|
|
457
|
+
arguments, `arguments` / `keywords` the transformed ones."""
|
|
458
|
+
slots = self.slots_by_name.get(pine_name)
|
|
459
|
+
if slots is None:
|
|
460
|
+
return arguments, keywords
|
|
461
|
+
scope = self.call_scopes.get(id(call))
|
|
462
|
+
if scope is None:
|
|
463
|
+
# Every Pine call was given a scope; a miss means a caller passed a
|
|
464
|
+
# rebuilt node, and the coercion would silently be skipped
|
|
465
|
+
raise RuntimeError(f"internal error (card #39): the call to overloaded '{pine_name}' "
|
|
466
|
+
f"(line {getattr(call, 'line', 0)}) was not seen when scopes were collected")
|
|
467
|
+
|
|
468
|
+
def coerced(slot, pine_argument, argument):
|
|
469
|
+
if (slots.includes(slot) and not isinstance(pine_argument, Literal)
|
|
470
|
+
and self.resolver.scalar_type(pine_argument, scope) == 'float'):
|
|
471
|
+
return FunctionCall(func='float', args=[argument], kwargs={})
|
|
472
|
+
return argument
|
|
473
|
+
return ([coerced(position, pine_argument, argument)
|
|
474
|
+
for position, (pine_argument, argument) in enumerate(zip(pine_arguments, arguments))],
|
|
475
|
+
{name: coerced(name, pine_keywords[name], argument) for name, argument in keywords.items()})
|