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,434 @@
1
+ """
2
+ Library collisions: a user binding named like a pynecore.lib name (card #9).
3
+
4
+ Pine keeps a variable, a parameter or a function apart from a built-in
5
+ namespace or function of the same name: `color.new(color, 50)` inside
6
+ `f(color = color.red)` calls the namespace on the parameter. Generated Python
7
+ has one namespace for both, and pynecore's import normalizer turns every read
8
+ of an imported name into the library object. The scope renamer therefore gives
9
+ such a binding a fresh name; this module decides which names collide and, for
10
+ each `N.m`, whether it reads the binding or the namespace N.
11
+ """
12
+ from typing import Callable, Dict, FrozenSet, List, Mapping, NamedTuple, Optional, Set
13
+
14
+ from .ast_nodes import (
15
+ Assignment, Expression, ExpressionStatement, ForInLoop, ForLoop, FuncDecl, FunctionCall, Identifier,
16
+ IfExpression, InputDecl, Literal, MemberAccess, NaLiteral, Parameter, Script, SwitchExpression,
17
+ TernaryOp, TupleDestructure, TypeDecl, VarDecl, VaripDecl,
18
+ )
19
+ from .ast_walk import walk
20
+ from .namespace_members import NAMESPACE_MEMBERS
21
+ from .pine_builtins import PYNECORE_LIB_MODULES
22
+ from .tokens import TYPE_KEYWORDS
23
+
24
+ NO_MEMBERS: FrozenSet[str] = frozenset()
25
+ # A constructor is called on the namespace, never as a method of a value:
26
+ # `line.new(...)` next to an array named `line`, `array.new_float(...)`, `array.from(...)`
27
+ CONSTRUCTORS = frozenset({'new', 'from'})
28
+ TYPED_CONSTRUCTOR_PREFIX = 'new_'
29
+ # `x.copy()` makes a value of x's type, and is a method of every object value
30
+ COPY_MEMBER = 'copy'
31
+
32
+
33
+ def colliding_names(script: Script) -> FrozenSet[str]:
34
+ """Names pynecore defines that the script binds itself (a variable, parameter, loop
35
+ variable, input or function). PyneComp renames every one of them, whether or not
36
+ the script reads the builtin (`pine_builtins.PYNECOMP_RESERVED_NAMES`)."""
37
+ from .pine_builtins import PYNECOMP_RESERVED_NAMES
38
+ bound: Set[str] = set()
39
+ for node in walk(script):
40
+ bound.update(_names_bound_by(node))
41
+ return frozenset(bound & PYNECOMP_RESERVED_NAMES)
42
+
43
+
44
+ def _names_bound_by(node):
45
+ if isinstance(node, (VarDecl, VaripDecl, InputDecl, Parameter)):
46
+ return (node.name,)
47
+ if isinstance(node, Assignment):
48
+ return node.target.names if isinstance(node.target, TupleDestructure) else (node.target,)
49
+ if isinstance(node, FuncDecl) and not node.is_method:
50
+ return (node.name,)
51
+ if isinstance(node, ForLoop):
52
+ return (node.var,)
53
+ if isinstance(node, ForInLoop):
54
+ return node.vars
55
+ return ()
56
+
57
+
58
+ def _dotted_head_or_callee(node) -> Optional[str]:
59
+ if isinstance(node, FunctionCall) and isinstance(node.func, str):
60
+ return node.func.split('.')[0]
61
+ if isinstance(node, Identifier) and '.' in node.name:
62
+ return node.name.split('.')[0]
63
+ if isinstance(node, MemberAccess) and isinstance(node.object, str):
64
+ return node.object.split('.')[0]
65
+ return None
66
+
67
+
68
+ class TypeEvidence(NamedTuple):
69
+ """What proves a value's type beyond its own declaration (card #91): the script's user type
70
+ names, the user type a user function provably returns, the type a variable holds where the
71
+ value is read (a container keeps its element type: `array<T>`), and the user method names (a
72
+ call by one may select the method, so it proves no element). Empty: the declaration alone."""
73
+ type_names: FrozenSet[str] = NO_MEMBERS
74
+ function_type: Callable[[str], Optional[str]] = lambda name: None
75
+ variable_type: Callable[[str], Optional[str]] = lambda name: None
76
+ method_names: FrozenSet[str] = NO_MEMBERS
77
+
78
+
79
+ NO_EVIDENCE = TypeEvidence()
80
+ # What `array.get(a, 0)`, `a.first()`, `matrix.get(m, 0, 0)` or `m.get(k)` returns: an element of the
81
+ # container (a map's value)
82
+ ELEMENT_ACCESSORS = {'array': frozenset({'get', 'first', 'last', 'pop', 'shift'}),
83
+ 'matrix': frozenset({'get'}), 'map': frozenset({'get'})}
84
+
85
+
86
+ def base_type(type_name: Optional[str]) -> Optional[str]:
87
+ """A type without its qualifier and element types: `array<T>` -> `array`, `series float` -> `float`."""
88
+ return type_name.split('<')[0].split()[-1] if type_name else None
89
+
90
+
91
+ def element_type(type_name: Optional[str]) -> Optional[str]:
92
+ """The type of a container's elements, a map's values: `array<T>`, `map<string, T>` -> `T`."""
93
+ if not type_name or '<' not in type_name or not type_name.endswith('>'):
94
+ return None
95
+ arguments, depth, start = type_name[type_name.index('<') + 1:-1], 0, 0
96
+ for index, character in enumerate(arguments):
97
+ depth += {'<': 1, '>': -1}.get(character, 0)
98
+ if character == ',' and depth == 0:
99
+ start = index + 1
100
+ return arguments[start:].strip() or None
101
+
102
+
103
+ def binding_type(type_hint: Optional[str], value, evidence: TypeEvidence = NO_EVIDENCE) -> Optional[str]:
104
+ """The Pine type a declaration gives its binding, as far as it is proven: its type annotation,
105
+ a constructor call (`P.new()`, `array.new<P>()`, `array.from(...)`), a literal, a call of a user
106
+ function that provably returns a user-defined type (`evidence.function_type`, from
107
+ `user_function_types`), or a value whose user type `evidence` proves (`_proven_user_type`).
108
+ A container keeps its element type (`array<P>`; `base_type` gives `array`). None when nothing
109
+ proves it: never a guess."""
110
+ if isinstance(value, FunctionCall) and isinstance(value.func, str) and '.' not in value.func:
111
+ return _without_qualifier(type_hint) if type_hint else evidence.function_type(value.func)
112
+ if type_hint:
113
+ return _without_qualifier(type_hint) # `series float`, `map<string, float>`
114
+ if isinstance(value, Literal):
115
+ return value.literal_type or None
116
+ constructed = _constructed_type(value, evidence)
117
+ return constructed if constructed is not None else _proven_user_type(value, evidence)
118
+
119
+
120
+ def _without_qualifier(type_hint: str) -> str:
121
+ prefix, bracket, elements = type_hint.partition('<')
122
+ return prefix.split()[-1] + bracket + elements
123
+
124
+
125
+ def _constructed_type(value, evidence: TypeEvidence) -> Optional[str]:
126
+ """`T.new()` -> T, `array.new<T>()` -> array<T>, `array.from(T.new())` -> array<T> when every
127
+ element is that user type; `p.copy()` copies a variable, its head names no type."""
128
+ if not (isinstance(value, FunctionCall) and isinstance(value.func, str) and '.' in value.func):
129
+ return None
130
+ head, _, member = value.func.rpartition('.')
131
+ makes_a_value = is_constructor(member) or member == COPY_MEMBER
132
+ if not makes_a_value or not (head in TYPE_KEYWORDS or head in PYNECORE_LIB_MODULES or head[:1].isupper()):
133
+ return None
134
+ _, bracket, elements = member.partition('<')
135
+ if bracket:
136
+ return head + bracket + elements
137
+ if head == 'array' and member == 'from' and value.args:
138
+ element_types = {_user_type(element, evidence) for element in value.args}
139
+ if len(element_types) == 1 and None not in element_types:
140
+ return f'array<{element_types.pop()}>'
141
+ return head
142
+
143
+
144
+ def _proven_user_type(value, evidence: TypeEvidence) -> Optional[str]:
145
+ """The user type `evidence` proves a value has (card #91): a variable that holds one, an element
146
+ of a container of one, or a ternary, switch or if-expression whose every branch that is not
147
+ `na` is that one type. Only a user type: a builtin type stays unknown here, as before."""
148
+ if isinstance(value, Identifier) and '.' not in value.name:
149
+ return _as_user_type(evidence.variable_type(value.name), evidence)
150
+ if isinstance(value, (TernaryOp, IfExpression, SwitchExpression)):
151
+ branch_types = {_branch_type(branch, evidence) for branch in _branches(value)} - {NA_BRANCH}
152
+ return branch_types.pop() if len(branch_types) == 1 else None
153
+ if isinstance(value, FunctionCall):
154
+ return _as_user_type(element_type(_accessed_container(value, evidence)), evidence)
155
+ return None
156
+
157
+
158
+ NA_BRANCH = 'na'
159
+
160
+
161
+ def _branches(value) -> list:
162
+ if isinstance(value, TernaryOp):
163
+ return [value.true_expr, value.false_expr]
164
+ if isinstance(value, IfExpression):
165
+ return [value.true_expr, value.false_expr] # no `else`: the value is na
166
+ return [body for _, body in value.cases] + [value.default]
167
+
168
+
169
+ def _branch_type(branch, evidence: TypeEvidence) -> Optional[str]:
170
+ """The user type of one branch, NA_BRANCH for `na`. A block's value is its last expression,
171
+ read with the block's own declarations: a block local hides the outer name it shares."""
172
+ if isinstance(branch, list):
173
+ branch, evidence = _block_value(branch, evidence)
174
+ if branch is None or isinstance(branch, NaLiteral):
175
+ return NA_BRANCH
176
+ return _user_type(branch, evidence)
177
+
178
+
179
+ def _block_value(block: list, evidence: TypeEvidence):
180
+ """A block's last expression (False when the block ends otherwise) and the evidence it is read
181
+ with: each declaration at the block's top typed in order; a name bound anywhere else in the
182
+ block (a nested block, a loop, a tuple) proves nothing."""
183
+ last = block[-1] if block else None
184
+ last = last.expr if isinstance(last, ExpressionStatement) else last
185
+ if not isinstance(last, Expression):
186
+ return False, evidence
187
+ local_types: Dict[str, Optional[str]] = {}
188
+ outer = evidence.variable_type
189
+ for statement in block[:-1]:
190
+ evidence_here = evidence._replace(
191
+ variable_type=lambda name, known=dict(local_types): known[name] if name in known else outer(name))
192
+ if isinstance(statement, (VarDecl, VaripDecl)) or (
193
+ isinstance(statement, Assignment) and not isinstance(statement.target, TupleDestructure)):
194
+ local_types.setdefault(statement.name if isinstance(statement, (VarDecl, VaripDecl))
195
+ else statement.target,
196
+ binding_type(statement.type_hint, statement.value, evidence_here))
197
+ for node in walk(block):
198
+ for name in _names_bound_by(node):
199
+ local_types.setdefault(name, None)
200
+ return last, evidence._replace(
201
+ variable_type=lambda name: local_types[name] if name in local_types else outer(name))
202
+
203
+
204
+ def _user_type(value, evidence: TypeEvidence) -> Optional[str]:
205
+ return _as_user_type(binding_type(None, value, evidence), evidence)
206
+
207
+
208
+ def _as_user_type(type_name: Optional[str], evidence: TypeEvidence) -> Optional[str]:
209
+ return type_name if type_name in evidence.type_names else None
210
+
211
+
212
+ def _accessed_container(call: FunctionCall, evidence: TypeEvidence) -> Optional[str]:
213
+ """The type of the container whose element `call` returns: `array.get(a, 0)` reads its first
214
+ argument, `a.first()` the variable `a`; None for any other call, or a method form whose
215
+ member a user method shares."""
216
+ if not isinstance(call.func, str) or '.' not in call.func:
217
+ return None
218
+ head, _, member = call.func.rpartition('.')
219
+ if head in ELEMENT_ACCESSORS:
220
+ kind = head
221
+ container = _container_type(call.args[0], evidence) if call.args else None
222
+ elif '.' not in head and member not in evidence.method_names:
223
+ container = evidence.variable_type(head)
224
+ kind = base_type(container)
225
+ else:
226
+ return None
227
+ return container if base_type(container) == kind and member in ELEMENT_ACCESSORS.get(kind, ()) else None
228
+
229
+
230
+ def _container_type(value, evidence: TypeEvidence) -> Optional[str]:
231
+ if isinstance(value, Identifier) and '.' not in value.name:
232
+ return evidence.variable_type(value.name)
233
+ return binding_type(None, value, evidence)
234
+
235
+
236
+ def script_type_evidence(script: Script) -> TypeEvidence:
237
+ """The evidence a whole script gives (no variables): its user types, user function returns
238
+ and user method names. A caller adds `variable_type` from its own scope model."""
239
+ return TypeEvidence(
240
+ frozenset(node.name for node in script.declarations if isinstance(node, TypeDecl)),
241
+ user_function_types(script).get, NO_MEMBERS,
242
+ frozenset(node.name for node in script.declarations if isinstance(node, FuncDecl) and node.is_method))
243
+
244
+
245
+ def user_function_types(script: Script) -> Dict[str, str]:
246
+ """User function name -> the user-defined type every declaration of it provably
247
+ returns (card #87): its last expression is a constructor or a copy of that type
248
+ (`mk() => T.new(close)`), a parameter or local declared with it, a call of such
249
+ a function, or a value whose type those prove (card #91: a ternary of that type, an
250
+ element of a container of it). A name a method shares is left out: a call by it may
251
+ select the method."""
252
+ type_names = frozenset(node.name for node in script.declarations if isinstance(node, TypeDecl))
253
+ functions: Dict[str, List[FuncDecl]] = {}
254
+ method_names = set()
255
+ for node in script.declarations:
256
+ if isinstance(node, FuncDecl):
257
+ if node.is_method:
258
+ method_names.add(node.name)
259
+ else:
260
+ functions.setdefault(node.name, []).append(node)
261
+ resolved: Dict[str, Optional[str]] = {}
262
+
263
+ def returned(name: str, visiting: FrozenSet[str]) -> Optional[str]:
264
+ if name in resolved:
265
+ return resolved[name]
266
+ if name not in functions or name in method_names or name in visiting:
267
+ return None
268
+ evidence = TypeEvidence(type_names, lambda callee: returned(callee, visiting | {name}),
269
+ method_names=frozenset(method_names))
270
+ returns = {_returned_type(declaration, evidence) for declaration in functions[name]}
271
+ result = returns.pop() if len(returns) == 1 else None
272
+ resolved[name] = result if result in type_names else None
273
+ return resolved[name]
274
+
275
+ return {name: type_name for name in functions
276
+ if (type_name := returned(name, frozenset())) is not None}
277
+
278
+
279
+ def _returned_type(function: FuncDecl, evidence: TypeEvidence) -> Optional[str]:
280
+ """The type of a function's last expression, as its declarations prove it: a name reads
281
+ the function's own parameters and locals only."""
282
+ local_types: Dict[str, Optional[str]] = {
283
+ parameter.name: binding_type(parameter.type_hint, None) for parameter in function.params}
284
+ body = function.body if isinstance(function.body, list) else [function.body]
285
+ for statement in body[:-1]:
286
+ if isinstance(statement, (VarDecl, VaripDecl)) or (
287
+ isinstance(statement, Assignment) and not isinstance(statement.target, TupleDestructure)):
288
+ name = statement.name if isinstance(statement, (VarDecl, VaripDecl)) else statement.target
289
+ local_types.setdefault(name, binding_type(statement.type_hint, statement.value,
290
+ evidence._replace(variable_type=dict(local_types).get)))
291
+ last = body[-1] if body else None
292
+ last = last.expr if isinstance(last, ExpressionStatement) else last
293
+ if isinstance(last, Identifier) and '.' not in last.name:
294
+ return local_types.get(last.name)
295
+ return binding_type(None, last, evidence._replace(variable_type=local_types.get))
296
+
297
+
298
+ def binding_types(script: Script) -> Dict[str, Optional[str]]:
299
+ """Every name the script binds, with the Pine type its declarations give it
300
+ (`declared_type`); None when no declaration says or two disagree."""
301
+ evidence = script_type_evidence(script)
302
+ types: Dict[str, Optional[str]] = {}
303
+ for node in walk(script):
304
+ for name in _names_bound_by(node):
305
+ declared = declared_type(node, evidence._replace(variable_type=types.get))
306
+ types[name] = declared if types.get(name, declared) == declared else None
307
+ return types
308
+
309
+
310
+ def global_declarations(script: Script) -> list:
311
+ """The declarations of globals (`var`/`varip`, inputs, top-level assignments) in source order,
312
+ the order the scope renamer binds and types them up front (card #91 review): a value reads the
313
+ globals declared above it, whichever kind each is. The parser keeps `var` declarations and
314
+ inputs apart from the body, so the two lists are merged by line."""
315
+ nodes = [node for node in list(script.declarations) + list(script.body)
316
+ if isinstance(node, (VarDecl, VaripDecl, InputDecl, Assignment))]
317
+ return sorted(nodes, key=lambda node: node.line)
318
+
319
+
320
+ def global_binding_types(script: Script) -> Dict[str, Optional[str]]:
321
+ """Every global name with the type of its first declaration, in the order the scope
322
+ renamer binds globals up front (`global_declarations`); a value reads the globals typed
323
+ before it."""
324
+ evidence = script_type_evidence(script)
325
+ types: Dict[str, Optional[str]] = {}
326
+ for node in global_declarations(script):
327
+ for name in _names_bound_by(node):
328
+ types.setdefault(name, declared_type(node, evidence._replace(variable_type=types.get)))
329
+ return types
330
+
331
+
332
+ def declared_type(node, evidence: TypeEvidence = NO_EVIDENCE) -> Optional[str]:
333
+ """The Pine type a declaring node gives the names it binds: a variable, an input, a
334
+ parameter (`binding_type`); None for a tuple, a loop variable or a function. An
335
+ untyped parameter's type comes from each call's argument, so it is None here."""
336
+ if isinstance(node, (VarDecl, VaripDecl)):
337
+ return binding_type(node.type_hint, node.value, evidence)
338
+ if isinstance(node, Assignment) and not isinstance(node.target, TupleDestructure):
339
+ return binding_type(node.type_hint, node.value, evidence)
340
+ if isinstance(node, Parameter):
341
+ return binding_type(node.type_hint, node.default)
342
+ if isinstance(node, InputDecl):
343
+ return input_type(node)
344
+ return None
345
+
346
+
347
+ def is_constructor(member: str) -> bool:
348
+ """`new`, `new<float>`, `new_float`, `from`: a namespace function that
349
+ makes a value (a field such as `newest` is not one)."""
350
+ name = member.split('<')[0]
351
+ return name in CONSTRUCTORS or name.startswith(TYPED_CONSTRUCTOR_PREFIX)
352
+
353
+
354
+ def input_type(declaration: InputDecl) -> str:
355
+ """An input's value type is named by its function: `input.int` -> int."""
356
+ return declaration.func.rpartition('.')[2]
357
+
358
+
359
+ class MemberRule:
360
+ """Decides, for `N.m` where N names a binding, whether `m` is a member of
361
+ the binding (a field or method of its type) or of the namespace N."""
362
+
363
+ def __init__(self, script: Script):
364
+ self.type_fields: Dict[str, FrozenSet[str]] = {
365
+ node.name: frozenset(field[0] for field in node.fields)
366
+ for node in script.declarations if isinstance(node, TypeDecl)}
367
+ # user method name -> the base types of its receiver (None: untyped)
368
+ self.method_receivers: Dict[str, Set[Optional[str]]] = {}
369
+ for node in script.declarations:
370
+ if isinstance(node, FuncDecl) and node.is_method and node.params:
371
+ receiver = base_type(binding_type(node.params[0].type_hint, None))
372
+ self.method_receivers.setdefault(node.name, set()).add(receiver)
373
+
374
+ def binding_owns(self, type_name: Optional[str], namespace: str, member: str) -> bool:
375
+ """True when `namespace.member` reads the binding named `namespace`,
376
+ whose type is `type_name` (None when unknown). `member` is Pine's
377
+ spelling (`from`, `new<float>`), so a constructor is told by its name. `type_name` may
378
+ keep its element type (`array<T>`): the decision reads its base."""
379
+ type_name = base_type(type_name)
380
+ receivers = self.method_receivers.get(member, ())
381
+ if receivers and (type_name is None or None in receivers or type_name in receivers):
382
+ return True
383
+ if is_constructor(member):
384
+ return False
385
+ if type_name is None:
386
+ # Unknown type: the binding's own member is what the namespace lacks. A member the
387
+ # namespace has stays the namespace's, never a guess (card #87 review): a binding
388
+ # holding a user type is known by its declaration (`binding_type`)
389
+ return member not in NAMESPACE_MEMBERS.get(namespace, NO_MEMBERS)
390
+ if type_name in self.type_fields:
391
+ return member in self.type_fields[type_name] or member == COPY_MEMBER
392
+ if type_name == namespace or (namespace, type_name) == ('str', 'string') or type_name not in TYPE_KEYWORDS:
393
+ # `matrix.set(matrix, ...)` on a matrix named matrix is the namespace call; so is `str.tostring(x)`
394
+ # where a string is named `str` (facejungle/utils)
395
+ return False
396
+ # A built-in type's methods are its namespace's functions: `a.size()` is array.size(a)
397
+ return member in NAMESPACE_MEMBERS.get(type_name, NO_MEMBERS)
398
+
399
+
400
+ class HeadBinding(NamedTuple):
401
+ """The binding a dotted head names where it is read: its Pine type, None when unknown."""
402
+ type_name: Optional[str]
403
+
404
+
405
+ class FieldReadRule:
406
+ """Decides whether the object of a history index reads a field of a user-defined
407
+ type (card #85): a member of an expression (`f().x`, `(t[1]).x`), or a dotted name
408
+ `N.m` whose head N is a binding that owns `m`. A head named like a namespace reads the
409
+ namespace unless MemberRule says `m` is the binding's own member: `session.ismarket`
410
+ beside an input named `session` reads the session namespace. The Pine v6 refusal
411
+ (pine_semantics.py) and the v5 lowering (grouped_history_lowering.py) both ask it.
412
+
413
+ The head's binding is the one in scope where the index is read, with the type its
414
+ declaration gives it (`declared_type`), as the scope renamer resolves `N.m` (card #88);
415
+ each caller says which binding that is, from its own scope model."""
416
+
417
+ def __init__(self, script: Script):
418
+ self.member_rule = MemberRule(script)
419
+
420
+ def is_field_read(self, value, binding_in_scope: Callable[[str], Optional[HeadBinding]]) -> bool:
421
+ """`binding_in_scope(head)`: the binding the head names where `value` is read, None
422
+ when it names no binding there."""
423
+ if isinstance(value, MemberAccess) and not isinstance(value.object, str):
424
+ return True
425
+ if isinstance(value, MemberAccess):
426
+ dotted = f'{value.object}.{value.member}'
427
+ else:
428
+ dotted = value.name if isinstance(value, Identifier) else ''
429
+ head, dot, path = dotted.partition('.')
430
+ binding = binding_in_scope(head) if dot else None
431
+ if binding is None:
432
+ return False
433
+ return head not in PYNECORE_LIB_MODULES or self.member_rule.binding_owns(
434
+ binding.type_name, head, path.partition('.')[0])