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