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,965 @@
1
+ """
2
+ Scope renamer: give every Pine local that shadows an outer name its own name,
3
+ and every binding named like a pynecore.lib name a name of its own.
4
+
5
+ In Pine, `x = ...` inside a function or a block declares a NEW local, even when
6
+ an outer `x` exists (the compiler only warns about the shadowing); `x := ...`
7
+ assigns the nearest visible `x`. Generated Python cannot express that directly:
8
+ an `if`/`for` block has no scope of its own, so a block local would overwrite
9
+ the global, and the transformer's symbol lookup would give a function local the
10
+ global's `var` persistence (card #38). This pre-pass renames each shadowing
11
+ declaration, and the references that follow it in the same scope, to a fresh
12
+ name, so every later stage sees distinct names. References before the
13
+ declaration keep the outer name, as in Pine.
14
+
15
+ A binding named like a pynecore.lib name that the script also uses as that
16
+ name (`f(color = color.red) => color.r(color)`) is renamed too, together with
17
+ its uses and the keyword arguments that name it; a declared function shadows
18
+ every bare call of its name (card #9). A function named like a builtin series
19
+ (`hour(x) => x * 3`, `close(x) => ...`) is renamed too whether or not the script reads
20
+ the series: its calls go to the new name and a bare read of the name stays the series (card #97). `N.m` keeps the namespace when `m` is
21
+ not the binding's own member (see library_collision.MemberRule). Two kinds of
22
+ binding keep their name: an input (its name is the key of the script's input
23
+ settings) and an `export` declaration (its name is the library's API); a
24
+ namespace use under such a binding reads an alias of the namespace instead.
25
+ An `export` with a parameter named like a function or method of the script is split into a private `f_body<N>`
26
+ (the same body, its parameters renamed like any function's) and the public export, which keeps the parameter names
27
+ and calls it (card #119).
28
+
29
+ A local named like a builtin series variable (`close`, `time`, `hour`) gets its own name
30
+ wherever it is declared below the top level: in a function, as a loop variable (card #84) or
31
+ in a block of a top-level `if`/`for`/`switch` (card #86). The script imports the builtin by
32
+ that name wherever the name is read, and pynecore then reads the library series for the
33
+ local's reads too; and a block local that kept the name would make every read of it in
34
+ main() a read of that local. A top-level declaration keeps its name (the import resolver
35
+ leaves that builtin out of the imports), and so does an input. Pine declares a global where
36
+ its declaration stands, so a read of its name before it (in a statement, a function declared
37
+ earlier, the declaration's own value `close = close * 2`, or an input's own arguments:
38
+ `close = input.source(close, ...)`) is the builtin: it reads the builtin by the namespace
39
+ alias (`_close_module`), inputs since card #86 and every other global since card #89. A bare
40
+ call of a builtin function named like such a global or a binding that keeps the name
41
+ (`time("D")`, `hour(t)`) calls the builtin by the alias too (card #89).
42
+
43
+ A Python reserved word used as a Pine name (`lambda`, `from`) gets its spelling here
44
+ too, once per name, against every identifier of the script (emitted_names.py, card
45
+ #77): `lambda` becomes `lambda_` when that is free, else `lambda_1`, so it never merges
46
+ with a user `lambda_`. Variables, parameters, functions, methods and type names share one
47
+ spelling domain; fields and enum members another (attributes), applied to every dotted
48
+ read, constructor keyword and call keyword of a user callable. An input and an `export`
49
+ declaration keep their raw name: the one existing sanitizer spells it, so a settings id
50
+ and a library API never move, and another binding holding that spelling moves instead.
51
+
52
+ A parameter is renamed for a library collision, and when it is named like a
53
+ script global (card #17): pynecore passes a global that a nested function reads
54
+ as an argument at each call site, so inside `f(g)` a callee `reader()` that
55
+ reads the global `g` would receive f's `g`. A keyword argument that names a
56
+ renamed parameter follows it; a builtin method or namespace function of the
57
+ same name keeps its own keyword.
58
+ """
59
+ import dataclasses
60
+ import re
61
+ from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Set, Tuple
62
+
63
+ from .ast_nodes import (
64
+ ASTNode, Assignment, EnumDecl, ForInLoop, ForLoop, FuncDecl, FunctionCall, Identifier,
65
+ IfExpression, IfStatement, InputDecl, Literal, MemberAccess, MethodCall, Reassignment,
66
+ Script, Statement, SwitchExpression, SwitchStatement, TupleDestructure, TypeDecl,
67
+ VarDecl, VaripDecl, WhileLoop,
68
+ )
69
+ from .ast_walk import walk
70
+ from .emitted_names import (
71
+ EMITTED_HELPER_NAMES, KeywordSpellings, LIBRARY_ALIAS_RENAMED_NAMES, free_spelling, identifier_names,
72
+ )
73
+ from .errors import UnsupportedFeatureError
74
+ from .node_ids import BlockNumbers, hex_suffixed
75
+ from .library_collision import (
76
+ MemberRule, binding_type, colliding_names, global_declarations, input_type, script_type_evidence,
77
+ )
78
+ from .pine_builtins import (
79
+ CALLABLE_SERIES_VARIABLES, GLOBAL_SUFFIX, PYNECOMP_RESERVED_NAMES, PYNECORE_LIB_MODULES, PYTHON_RESERVED_WORDS,
80
+ RENAMED_SUFFIX,
81
+ SERIES_VARIABLES,
82
+ sanitize_identifier,
83
+ )
84
+
85
+ FRESH_NAME_SUFFIX = '_local'
86
+ NAMESPACE_ALIAS = '_{}_module'
87
+ # The heads of a namespace function call: `array.get(...)`, Pine's `str.format(...)`
88
+ NAMESPACE_HEADS = PYNECORE_LIB_MODULES | {'str'}
89
+ SCOPED_STATEMENT_TYPES = (Assignment, VarDecl, VaripDecl, Reassignment, IfStatement,
90
+ ForLoop, ForInLoop, WhileLoop, SwitchStatement, FuncDecl)
91
+
92
+
93
+ class RenamedScript(NamedTuple):
94
+ script: Script
95
+ # pynecore.lib name -> the alias its namespace uses under a binding that keeps the name
96
+ namespace_aliases: Dict[str, str]
97
+ # function or method name -> {Pine parameter name: its emitted name}
98
+ parameter_names: Dict[str, Dict[str, str]]
99
+
100
+
101
+ @dataclasses.dataclass(frozen=True)
102
+ class Binding:
103
+ """What a Pine name in a scope stands for."""
104
+ emitted: str
105
+ collides: bool = False # named like a pynecore.lib name the script also uses
106
+ type_name: Optional[str] = None # the Pine type, when proven; a container keeps its element type
107
+
108
+
109
+ def rename_shadowing_locals(script: Script) -> RenamedScript:
110
+ """Return a copy of `script` with shadowing locals and library-named
111
+ bindings renamed. The input script is not modified."""
112
+ renamer = _ScopeRenamer(script)
113
+ return RenamedScript(renamer.run(script), renamer.namespace_aliases,
114
+ {name: renames for name, renames in renamer.parameter_names.items() if renames})
115
+
116
+
117
+ def collect_names(tree) -> Set[str]:
118
+ """Every identifier-like string in the tree: the names a fresh name must avoid."""
119
+ names: Set[str] = set()
120
+ _names_in(tree, names)
121
+ return names
122
+
123
+
124
+ def script_global_names(script: Script) -> Set[str]:
125
+ """The names a script declares at its top level: inputs, `var`/`varip`
126
+ declarations and assignments (tuple targets included)."""
127
+ names = {declaration.name for declaration in script.declarations
128
+ if isinstance(declaration, (VarDecl, VaripDecl, InputDecl))}
129
+ for statement in script.body:
130
+ if isinstance(statement, Assignment):
131
+ target = statement.target
132
+ names.update(target.names if isinstance(target, TupleDestructure) else [target])
133
+ return names
134
+
135
+
136
+ def _declared_in(statements) -> Set[str]:
137
+ """The names a block's own statements declare (not its nested blocks')."""
138
+ names: Set[str] = set()
139
+ if not isinstance(statements, list):
140
+ return names
141
+ for statement in statements:
142
+ if isinstance(statement, (VarDecl, VaripDecl)):
143
+ names.add(statement.name)
144
+ elif isinstance(statement, Assignment):
145
+ target = statement.target
146
+ names.update(target.names if isinstance(target, TupleDestructure) else [target])
147
+ return names
148
+
149
+
150
+ def _top_level_names(script: Script) -> Set[str]:
151
+ """Every name the script's top level declares, wherever its statement was moved."""
152
+ names: Set[str] = set()
153
+ for statement in list(script.declarations) + list(script.body):
154
+ if isinstance(statement, (VarDecl, VaripDecl, InputDecl)):
155
+ names.add(statement.name)
156
+ elif isinstance(statement, Assignment):
157
+ target = statement.target
158
+ names.update(target.names if isinstance(target, TupleDestructure) else [target])
159
+ return names
160
+
161
+
162
+ def builtin_series_globals(script: Script) -> Dict[str, Any]:
163
+ """Each global named like a builtin series variable -> its first top-level declaration (an
164
+ input, a `var`/`varip`, an assignment or a tuple target). The import resolver leaves such a
165
+ builtin out of the imports, so a read of it above the declaration needs the alias."""
166
+ declarations = [node for node in script.declarations if isinstance(node, (VarDecl, VaripDecl, InputDecl))]
167
+ declarations += [node for node in script.body if isinstance(node, Assignment)]
168
+ first: Dict[str, Any] = {}
169
+ for node in sorted(declarations, key=lambda node: node.line):
170
+ target = node.target if isinstance(node, Assignment) else node.name
171
+ for name in target.names if isinstance(target, TupleDestructure) else [target]:
172
+ if name in SERIES_VARIABLES:
173
+ first.setdefault(name, node)
174
+ return first
175
+
176
+
177
+ def inner_path_segments(script: Script) -> Set[str]:
178
+ """The segments between the head and the last name of every dotted name of `script`."""
179
+ segments: Set[str] = set()
180
+ for node in walk(script):
181
+ dotted = None
182
+ if isinstance(node, FunctionCall) and isinstance(node.func, str):
183
+ dotted = node.func
184
+ elif isinstance(node, Identifier):
185
+ dotted = node.name
186
+ elif isinstance(node, MemberAccess) and isinstance(node.object, str):
187
+ dotted = f'{node.object}.{node.member}'
188
+ if dotted and dotted.count('.') >= 2:
189
+ segments.update(dotted.split('.')[1:-1])
190
+ return segments
191
+
192
+
193
+ HEX_RENAME = re.compile(r'__[0-9a-f]{8}__')
194
+ #: the built-in types a user type may not be named like, compared without case
195
+ BUILTIN_TYPE_NAMES = frozenset({'box', 'label', 'line', 'linefill', 'table'})
196
+
197
+
198
+ def builtin_call_segments(script: Script) -> Set[str]:
199
+ """The receiver names of the dotted calls of a builtin method (`this.msi.put(k, v)`: `msi`): PyneComp
200
+ renames a local or parameter named like one, not for a field read (`this.sess.timeRanges`) nor for a
201
+ method no builtin namespace has (`this.channelDesc.validate()`) (probes fp7, fp13..fp15, chrono_utils,
202
+ signal_datagram)."""
203
+ from .pine_builtins import method_namespaces
204
+ # a field holding an array, a map or a matrix: any method called on it goes through that namespace's
205
+ # type (Trendoscope/Zigzag `candle.indicators.init(...)`)
206
+ collection_fields = {field[0] for node in walk(script) if isinstance(node, TypeDecl) for field in node.fields
207
+ if str(field[1]).startswith(('array<', 'map<', 'matrix<')) or str(field[1]).endswith('[]')}
208
+ segments: Set[str] = set()
209
+ for node in walk(script):
210
+ if isinstance(node, FunctionCall) and isinstance(node.func, str) and node.func.count('.') >= 2:
211
+ *_, receiver, member = node.func.split('.')
212
+ if method_namespaces(member) or receiver in collection_fields: # a builtin method, or a collection's
213
+ segments.add(receiver) # the receiver's own name only: `this.sess.timeRanges.size()` is `timeRanges`
214
+ return segments
215
+
216
+
217
+ def fresh_name(stem: str, used_names: Set[str]) -> str:
218
+ """`stem` plus the lowest index not in `used_names`; the result is recorded
219
+ as used. Ends in digits, so never a Python keyword nor a
220
+ sanitize_identifier result."""
221
+ index = 1
222
+ while f'{stem}{index}' in used_names:
223
+ index += 1
224
+ name = f'{stem}{index}'
225
+ used_names.add(name)
226
+ return name
227
+
228
+
229
+ def _names_in(value, names: Set[str]) -> None:
230
+ """Collect every identifier-like string in the tree (first dotted segment)."""
231
+ if isinstance(value, str):
232
+ names.add(value.split('.')[0])
233
+ elif isinstance(value, (list, tuple)):
234
+ for item in value:
235
+ _names_in(item, names)
236
+ elif isinstance(value, dict):
237
+ for item in value.values():
238
+ _names_in(item, names)
239
+ elif dataclasses.is_dataclass(value):
240
+ for field in dataclasses.fields(value):
241
+ _names_in(getattr(value, field.name), names)
242
+
243
+
244
+ class _ScopeRenamer:
245
+ """Walks a Script with a stack of scopes, each mapping a Pine name to its
246
+ Binding."""
247
+
248
+ def __init__(self, script: Script):
249
+ self.used_names = collect_names(script)
250
+ self.scopes: List[Dict[str, Binding]] = []
251
+ self.scope_declarations: List[Set[str]] = [] # per scope: every name its block declares
252
+ # PyneComp's id of the block of each scope in `scopes` (0: the top level, or not known)
253
+ self.block_numbers = BlockNumbers(script)
254
+ self.scope_blocks: List[int] = []
255
+ # the index in `scopes` of each function scope being visited, innermost last
256
+ self.function_scopes: List[int] = []
257
+ self.shadowed_names: Set[str] = set()
258
+ self.library_names = colliding_names(script)
259
+ self.top_level_names = _top_level_names(script)
260
+ self.renamed_exports: Set[str] = set() # exported functions spelled `name__ren__`
261
+ self.member_rule = MemberRule(script)
262
+ # the user types, user function returns and user methods that prove a binding's type: the
263
+ # type of `x = mk()` (card #87), `x = array.get(a, 0)` or `x = c ? T.new() : na` (card #91)
264
+ self.type_evidence = script_type_evidence(script)
265
+ self.namespace_aliases: Dict[str, str] = {}
266
+ # a library alias the compiled module binds itself -> its emitted name (card #111)
267
+ self.library_alias_spellings: Dict[str, str] = {}
268
+ # user function named like a library name or a builtin series -> its emitted name (shared by its overloads)
269
+ self.function_names: Dict[str, str] = {}
270
+ # function or method name -> {renamed parameter: its emitted name}
271
+ self.parameter_names: Dict[str, Dict[str, str]] = {}
272
+ self.method_names = {node.name for node in script.declarations
273
+ if isinstance(node, FuncDecl) and node.is_method}
274
+ self.user_function_names = {node.name for node in script.declarations
275
+ if isinstance(node, FuncDecl) and not node.is_method}
276
+ # Python reserved words used as Pine names get one spelling each, chosen against the
277
+ # identifiers of the script (never its string literals or titles) and the names the
278
+ # generated module defines itself (card #77)
279
+ self.spellings = KeywordSpellings(script, identifier_names(script) | EMITTED_HELPER_NAMES)
280
+ # the declared UDT and enum names: the heads of `T.new(...)`, `Color.red`
281
+ self.type_names = {node.name for node in script.declarations if isinstance(node, (TypeDecl, EnumDecl))}
282
+ # the inner segments of the dotted names the script uses (`risk` of `strategy.risk.allow_entry_in`)
283
+ self.namespace_segments = inner_path_segments(script)
284
+ self.builtin_call_segments = builtin_call_segments(script)
285
+ # keyword-named UDT/enum -> its spelling, for type-hint tokens
286
+ self.type_tokens = {name: self.spellings.variable(name) for name in sorted(self.type_names)
287
+ if name in PYTHON_RESERVED_WORDS}
288
+ # a user type named like a built-in drawing type, whatever its case, is `Box__ren__` (PyneComp:
289
+ # Trendoscope/Drawing `Line__ren__`, `Linefill__ren__`; `PolyLine` keeps its name)
290
+ self.type_tokens.update({name: f'{name}{RENAMED_SUFFIX}' for name in self.type_names
291
+ if name.lower() in BUILTIN_TYPE_NAMES and name not in self.type_tokens})
292
+ # sanitized spellings of keyword-named inputs and exports: the settings id and the
293
+ # library API keep them, so another binding of that spelling moves
294
+ self.external_spellings: Set[str] = set()
295
+ # keyword-named input whose spelling another input holds, or input named like a function or method of
296
+ # the script -> its new name
297
+ self.moved_inputs: Dict[str, str] = {}
298
+ # the inputs named like a function or method of the script: they keep their Pine label (card #119)
299
+ self.inputs_named_like_a_function: Set[str] = set()
300
+ self.moved_bindings = False
301
+ # global named like a builtin series -> its first declaration: an input (card #86), a
302
+ # var, an assignment or a tuple target (card #89)
303
+ self.builtin_globals: Dict[str, Any] = builtin_series_globals(script)
304
+ self.plan_external_names(script)
305
+ self.plan_functions(script)
306
+
307
+ # ---------------------------------------------------------------- names
308
+
309
+ def fresh_name(self, name: str) -> str:
310
+ return fresh_name(f'{name}{FRESH_NAME_SUFFIX}', self.used_names)
311
+
312
+ def global_name(self, name: str) -> str:
313
+ """PyneComp's spelling of a global it renames: `name__global__`."""
314
+ spelling = f'{name}{GLOBAL_SUFFIX}'
315
+ if spelling in self.used_names:
316
+ return self.fresh_name(name)
317
+ self.used_names.add(spelling)
318
+ return spelling
319
+
320
+ def block_local_name(self, name: str, block_id: int) -> str:
321
+ """PyneComp's spelling of a renamed local: `name__<its block's id>__`."""
322
+ if not block_id:
323
+ return self.fresh_name(name)
324
+ spelling = hex_suffixed(name, block_id)
325
+ if spelling in self.used_names:
326
+ return self.fresh_name(name)
327
+ self.used_names.add(spelling)
328
+ return spelling
329
+
330
+ def reserved_name(self, name: str) -> str:
331
+ """PyneComp's spelling of a binding named like something pynecore defines: one per name."""
332
+ spelling = f'{name}{RENAMED_SUFFIX}'
333
+ self.used_names.add(spelling)
334
+ return spelling
335
+
336
+ def lookup(self, name: str) -> Optional[Binding]:
337
+ for scope in reversed(self.scopes):
338
+ if name in scope:
339
+ return scope[name]
340
+ return None
341
+
342
+ def resolve(self, name: str) -> str:
343
+ binding = self.lookup(name)
344
+ return binding.emitted if binding else name
345
+
346
+ def resolve_dotted(self, dotted: str, is_call: bool = False) -> str:
347
+ """`t.bx` -> the emitted name of `t` plus `.bx`; `color.new` on a
348
+ library-named binding `color` stays the namespace unless `new` is the
349
+ binding's own member. The segments after a user binding or a declared
350
+ type are its fields and members (a keyword spelling follows the field map);
351
+ the last one of a call is a method."""
352
+ first, dot, rest = dotted.partition('.')
353
+ binding = self.lookup(first)
354
+ if binding is None:
355
+ if dot and first in self.type_names:
356
+ return self.type_spelling(first) + dot + self.member_path(rest, is_call)
357
+ return dotted
358
+ if dot and binding.collides and not self.member_rule.binding_owns(
359
+ binding.type_name, first, rest.partition('.')[0]):
360
+ return self.namespace(first) + dot + rest
361
+ return binding.emitted + dot + self.member_path(rest, is_call)
362
+
363
+ def member_path(self, path: str, is_call: bool = False) -> str:
364
+ """The emitted spelling of `a.b.c` after the head: fields, and a user method last in a call."""
365
+ if not path:
366
+ return path
367
+ *fields, last = path.split('.')
368
+ last = self.method_spelling(last) if is_call else self.spellings.attribute(last)
369
+ return '.'.join([self.spellings.attribute(name) for name in fields] + [last])
370
+
371
+ def method_spelling(self, name: str) -> str:
372
+ """A user method's emitted name; a builtin method keeps its own."""
373
+ return self.function_names.get(name, name) if name in self.method_names else name
374
+
375
+ def value_type(self, type_hint: Optional[str], value) -> Optional[str]:
376
+ """The Pine type a declaration gives its binding (`binding_type`, with the script's evidence
377
+ and the type of each variable in scope where the declaration stands)."""
378
+ return binding_type(type_hint, value, self.type_evidence._replace(variable_type=self.variable_type))
379
+
380
+ def variable_type(self, name: str) -> Optional[str]:
381
+ binding = self.lookup(name)
382
+ return binding.type_name if binding else None
383
+
384
+ def type_spelling(self, name: str) -> str:
385
+ return self.type_tokens.get(name, name)
386
+
387
+ def type_hint(self, hint: Optional[str]) -> Optional[str]:
388
+ """A type-hint string with the keyword-named user types it mentions respelled."""
389
+ if hint is None or not (self.type_tokens or self.library_alias_spellings):
390
+ return hint
391
+ # a type a library exports (`lib.T`) is spelled through the alias's emitted name
392
+ hint = re.sub(r'\b([A-Za-z_]\w*)(?=\.)',
393
+ lambda match: self.library_alias_spellings.get(match.group(1), match.group(1)), hint)
394
+ return re.sub(r'\b[A-Za-z_]\w*\b', lambda match: self.type_tokens.get(match.group(0), match.group(0)), hint)
395
+
396
+ def namespace(self, name: str) -> str:
397
+ """How the namespace `name` is spelled here: as itself, or by an alias
398
+ when a visible binding keeps the name (an input, an export)."""
399
+ if not any(name in scope and scope[name].emitted == name for scope in self.scopes):
400
+ return name
401
+ return self.alias(name)
402
+
403
+ def alias(self, name: str) -> str:
404
+ """The alias the module imports the pynecore.lib name `name` by."""
405
+ self.moved_bindings = True
406
+ alias = self.namespace_aliases.get(name)
407
+ if alias is None:
408
+ alias = NAMESPACE_ALIAS.format(name)
409
+ if alias in self.used_names:
410
+ alias = fresh_name(alias, self.used_names)
411
+ self.used_names.add(alias)
412
+ self.namespace_aliases[name] = alias
413
+ return alias
414
+
415
+ def callee(self, name: str) -> str:
416
+ """The emitted name of a bare call `name(...)`: the declared function of
417
+ that name, which shadows every bare call of it; else the library
418
+ function, by its alias when a visible input or export keeps the name
419
+ (Pine keeps functions and variables apart, Python does not). A builtin
420
+ function named like a builtin series (`time("D")`) is called by the alias
421
+ beside a binding that keeps the name, or a global of that name (card #89)."""
422
+ if name in self.function_names:
423
+ return self.function_names[name]
424
+ builtin_function = name in CALLABLE_SERIES_VARIABLES and name not in self.user_function_names
425
+ if builtin_function and name in self.builtin_globals:
426
+ return self.alias(name) # the module does not import it (card #89)
427
+ if name in self.library_names or builtin_function:
428
+ return self.namespace(name)
429
+ return name
430
+
431
+ def declare(self, name: str, type_name: Optional[str] = None) -> str:
432
+ """Bind `name` in the innermost scope; a fresh name if it shadows an
433
+ outer binding, collides with a library name, or hides a builtin series."""
434
+ if name == '_':
435
+ return name # Pine's discarded tuple element is never renamed (PyneComp)
436
+ if name in self.scopes[-1]:
437
+ return self.scopes[-1][name].emitted
438
+ # Only a binding of the same Python function is shadowed: a function's own local
439
+ # keeps the name of a global it hides, as PyneComp keeps it
440
+ first = self.function_scopes[-1] if self.function_scopes else 0
441
+ shadows = any(name in scope for scope in self.scopes[first:-1])
442
+ if self.function_scopes:
443
+ # a declaration the enclosing blocks of this function make later counts too
444
+ shadows = shadows or any(name in names for names in self.scope_declarations[first:-1])
445
+ # in a block nested in a function, a local named like a global of the script takes its block's id, while
446
+ # the function's own body keeps the name (probe sh1, HeWhoMustNotBeNamed/enhanced_ta)
447
+ shadows = shadows or (bool(self.function_scopes) and len(self.scopes) - 1 > first
448
+ and name in self.top_level_names)
449
+ if shadows:
450
+ self.shadowed_names.add(name)
451
+ # scopes[0] is the top level; every deeper scope is a function, a loop or a block
452
+ # a variable named like a function or method of the script: Python reads the name as the variable all through
453
+ # its function, so a call of the function in that scope reads the variable (card #118). At the top level too:
454
+ # the functions are defined inside main(), where the variable is a local that hides them from every function
455
+ names_a_function = name in self.user_function_names or name in self.method_names
456
+ if names_a_function:
457
+ self.moved_bindings = True
458
+ collides = name in self.library_names
459
+ hides_builtin_series = name in SERIES_VARIABLES and len(self.scopes) > 1
460
+ if hides_builtin_series:
461
+ self.moved_bindings = True
462
+ # a global named like a type of the script or a namespace it uses (PyneComp: `snd__global__`, `risk__global__`)
463
+ names_a_global_entity = len(self.scopes) == 1 and (name in self.type_names or name in self.namespace_segments)
464
+ if collides and len(self.scopes) > 1 and (shadows or names_a_function):
465
+ # a pynecore name that also hides a function or an outer binding: both spellings
466
+ # (`size__ren____00000038__`, forex_factory_utility)
467
+ emitted = hex_suffixed(self.reserved_name(name), self.scope_blocks[-1] if self.scope_blocks else 0)
468
+ elif collides:
469
+ emitted = self.reserved_name(name)
470
+ elif len(self.scopes) == 1 and (names_a_function or names_a_global_entity):
471
+ self.moved_bindings = True
472
+ emitted = self.global_name(name)
473
+ elif shadows or hides_builtin_series or names_a_function or name in self.builtin_call_segments:
474
+ emitted = self.block_local_name(name, self.scope_blocks[-1] if self.scope_blocks else 0)
475
+ elif self.holds_external_spelling(name):
476
+ self.moved_bindings = True
477
+ emitted = self.fresh_name(name)
478
+ else:
479
+ emitted = self.spellings.variable(name)
480
+ self.scopes[-1][name] = Binding(emitted, collides, type_name)
481
+ return emitted
482
+
483
+ def holds_external_spelling(self, name: str) -> bool:
484
+ """Whether `name` is the spelling an input, an export or an export parameter keeps
485
+ (`lambda_` for `lambda`): the binding that merely shares it moves."""
486
+ if not name.endswith('_') or name[:-1] not in PYTHON_RESERVED_WORDS:
487
+ return False
488
+ return name in self.external_spellings or any(
489
+ binding.emitted == name and key != name
490
+ for scope in self.scopes for key, binding in scope.items())
491
+
492
+ def keep(self, name: str, type_name: Optional[str]) -> Binding:
493
+ """The binding of a name that must not change (an input, an export)."""
494
+ return Binding(name, name in self.library_names, type_name)
495
+
496
+ # ---------------------------------------------------------------- scopes
497
+
498
+ def scoped_block(self, statements, declared: Iterable[str] = (),
499
+ parameters: Dict[str, Binding] = None, block_id: int = 0) -> Tuple[object, List[str]]:
500
+ """Visit a nested scope. `parameters` bind as given; `declared` (loop
501
+ variables) bind like declarations. Returns the new body and the
502
+ emitted names of `declared`."""
503
+ self.scopes.append(dict(parameters or {}))
504
+ self.scope_blocks.append(block_id or self.block_numbers.of(statements))
505
+ # what this block declares anywhere in it: a nested block's local shadows it even when the
506
+ # declaration comes later (analytics_tables `float ret` after an inner loop's `float ret`)
507
+ self.scope_declarations.append(_declared_in(statements))
508
+ try:
509
+ emitted_declared = [self.declare(name) for name in declared]
510
+ if isinstance(statements, list):
511
+ body = [self.statement(item) for item in statements]
512
+ else:
513
+ body = self.expression(statements)
514
+ finally:
515
+ self.scopes.pop()
516
+ self.scope_blocks.pop()
517
+ self.scope_declarations.pop()
518
+ return body, emitted_declared
519
+
520
+ def block(self, statements):
521
+ if statements is None:
522
+ return None
523
+ return self.scoped_block(statements)[0]
524
+
525
+ # ---------------------------------------------------------------- statements
526
+
527
+ def statement(self, node):
528
+ replace = dataclasses.replace
529
+ if isinstance(node, Assignment):
530
+ value = self.expression(node.value)
531
+ if isinstance(node.target, TupleDestructure):
532
+ names = [self.declare(name) for name in node.target.names]
533
+ return replace(node, value=value, target=replace(node.target, names=names, value=value))
534
+ return replace(node, value=value, type_hint=self.type_hint(node.type_hint),
535
+ target=self.declare(node.target, self.value_type(node.type_hint, node.value)))
536
+ if isinstance(node, (VarDecl, VaripDecl)):
537
+ value = self.expression(node.value)
538
+ return replace(node, value=value, type_hint=self.type_hint(node.type_hint),
539
+ name=self.declare(node.name, self.value_type(node.type_hint, node.value)))
540
+ if isinstance(node, Reassignment):
541
+ return replace(node, value=self.expression(node.value),
542
+ target=self.resolve_dotted(node.target))
543
+ if isinstance(node, IfStatement):
544
+ return replace(node, condition=self.expression(node.condition),
545
+ body=self.block(node.body),
546
+ elseifs=[(self.expression(condition), self.block(body))
547
+ for condition, body in node.elseifs],
548
+ else_body=self.block(node.else_body))
549
+ if isinstance(node, (ForLoop, ForInLoop)):
550
+ return self.loop(node)
551
+ if isinstance(node, WhileLoop):
552
+ return replace(node, condition=self.expression(node.condition),
553
+ body=self.block(node.body))
554
+ if isinstance(node, SwitchStatement):
555
+ return replace(node, expr=self.expression(node.expr),
556
+ cases=[(self.expression(case), self.block(body))
557
+ for case, body in node.cases],
558
+ default=self.block(node.default))
559
+ if isinstance(node, FuncDecl):
560
+ return self.function(node)
561
+ return self.expression(node)
562
+
563
+ def loop(self, node):
564
+ replace = dataclasses.replace
565
+ if isinstance(node, ForLoop):
566
+ from_value, to_value, step = (self.expression(node.from_val),
567
+ self.expression(node.to_val),
568
+ self.expression(node.step))
569
+ body, (variable,) = self.scoped_block(node.body, declared=[node.var])
570
+ return replace(node, var=variable, from_val=from_value, to_val=to_value,
571
+ step=step, body=body)
572
+ iterable = self.expression(node.iterable)
573
+ body, variables = self.scoped_block(node.body, declared=node.vars)
574
+ return replace(node, vars=variables, iterable=iterable, body=body)
575
+
576
+ def function(self, node: FuncDecl) -> FuncDecl:
577
+ """A function's parameters and body share one scope. A default is
578
+ evaluated where the function is declared, so it is visited first."""
579
+ renames = self.parameter_names.get(node.name, {})
580
+ parameters = []
581
+ bindings: Dict[str, Binding] = {}
582
+ for parameter in node.params:
583
+ type_name = binding_type(parameter.type_hint, parameter.default)
584
+ if parameter.name not in renames:
585
+ binding = self.keep(parameter.name, type_name)
586
+ else:
587
+ emitted = renames[parameter.name]
588
+ block_id = self.block_numbers.of(node)
589
+ if HEX_RENAME.fullmatch(emitted[len(parameter.name):]) and block_id:
590
+ emitted = hex_suffixed(parameter.name, block_id) # each overload its own block (chrono_utils)
591
+ binding = Binding(emitted, parameter.name in self.library_names, type_name)
592
+ bindings[parameter.name] = binding
593
+ parameters.append(dataclasses.replace(parameter, name=binding.emitted,
594
+ type_hint=self.type_hint(parameter.type_hint),
595
+ default=self.expression(parameter.default)))
596
+ self.function_scopes.append(len(self.scopes))
597
+ try:
598
+ body, _ = self.scoped_block(node.body, parameters=bindings, block_id=self.block_numbers.of(node))
599
+ finally:
600
+ self.function_scopes.pop()
601
+ name = (self.function_names.get(node.name, node.name) if not node.is_export or node.name in self.renamed_exports
602
+ else node.name)
603
+ return dataclasses.replace(node, name=name, params=parameters, body=body,
604
+ return_type=self.type_hint(node.return_type))
605
+
606
+ def plan_functions(self, script: Script) -> None:
607
+ """Fresh names for user functions named like a library name or a builtin
608
+ series (every overload shares one) and their parameters named like a library name or a script global
609
+ (one per function name, so a keyword argument maps the same way to every
610
+ overload). An `export` declaration keeps its names."""
611
+ global_names = script_global_names(script)
612
+ # an `export` overload keeps its parameters, so its name's keywords must too
613
+ exported = {declaration.name for declaration in script.declarations
614
+ if isinstance(declaration, FuncDecl) and declaration.is_export}
615
+ for declaration in script.declarations:
616
+ if not isinstance(declaration, FuncDecl):
617
+ continue
618
+ name = declaration.name
619
+ if declaration.is_export:
620
+ if (name in PYNECOMP_RESERVED_NAMES or name in SERIES_VARIABLES) and name not in self.function_names:
621
+ # PyneComp: an exported function or method named like a pynecore name is `fill__ren__`
622
+ self.function_names[name] = self.reserved_name(name)
623
+ self.renamed_exports.add(name)
624
+ self.plan_export_parameters(declaration)
625
+ # PyneComp renames an exported parameter named like a pynecore name too (`order__ren__`)
626
+ renames = self.parameter_names.setdefault(declaration.name, {})
627
+ for parameter in declaration.params:
628
+ if parameter.name in self.library_names - SERIES_VARIABLES and parameter.name not in renames:
629
+ renames[parameter.name] = self.reserved_name(parameter.name)
630
+ if parameter.name in self.user_function_names | self.method_names:
631
+ # and named like a function too (`method__ren____00000046__`, FunctionZigZagMultipleMethods)
632
+ renames[parameter.name] = hex_suffixed(renames[parameter.name],
633
+ self.block_numbers.of(declaration))
634
+ elif (parameter.name in self.user_function_names | self.method_names | self.builtin_call_segments
635
+ and parameter.name not in renames):
636
+ # and one named like a function or method of the script, as any local (`smoothrng__0000001e__`)
637
+ renames[parameter.name] = self.block_local_name(parameter.name,
638
+ self.block_numbers.of(declaration))
639
+ continue
640
+ name = declaration.name
641
+ # a function named like a pynecore.lib name the script uses, or like a builtin series (card
642
+ # #97): the module imports the builtin wherever its name is read, and pynecore reads the
643
+ # library object for every use of an imported name, the function's calls included
644
+ if ((name in self.library_names or name in SERIES_VARIABLES)
645
+ and not declaration.is_method and name not in self.function_names):
646
+ # PyneComp spells such a function `name__global__` (`def position__global__(...)`)
647
+ self.function_names[name] = self.global_name(name)
648
+ elif name not in self.function_names and (
649
+ name in PYTHON_RESERVED_WORDS or name in self.external_spellings):
650
+ # one spelling for every overload; a function holding an external spelling moves
651
+ self.function_names[name] = (self.spellings.variable(name) if name in PYTHON_RESERVED_WORDS
652
+ else self.fresh_name(name))
653
+ renames = self.parameter_names.setdefault(name, {})
654
+ # a parameter named like a script global keeps its name (PyneComp); one named like a
655
+ # pynecore name is renamed, except a builtin series, which the function body reads as the parameter
656
+ reserved_parameters = self.library_names - SERIES_VARIABLES
657
+ renamed_names = set(self.external_spellings)
658
+ if name not in exported:
659
+ # a parameter named like a function or method of the script hides it from the body (card #119)
660
+ renamed_names = renamed_names | self.user_function_names | self.method_names
661
+ # and one named like an inner segment of a dotted call of a builtin (`this.msi.put(...)`: `msi__00000006__`)
662
+ renamed_names = renamed_names | self.builtin_call_segments
663
+ for parameter in declaration.params:
664
+ if parameter.name in reserved_parameters and parameter.name not in renames:
665
+ renames[parameter.name] = self.reserved_name(parameter.name)
666
+ elif parameter.name in renamed_names and parameter.name not in renames:
667
+ renames[parameter.name] = self.block_local_name(parameter.name,
668
+ self.block_numbers.of(declaration))
669
+ elif parameter.name in PYTHON_RESERVED_WORDS and parameter.name not in renames:
670
+ renames[parameter.name] = self.spellings.variable(parameter.name)
671
+
672
+ def plan_export_parameters(self, declaration: FuncDecl) -> None:
673
+ """A keyword-named parameter of an `export` function keeps its API spelling
674
+ (`lambda_`): the definition and the call keywords of this script use it."""
675
+ raw_names = {parameter.name for parameter in declaration.params}
676
+ renames = self.parameter_names.setdefault(declaration.name, {})
677
+ for parameter in declaration.params:
678
+ if parameter.name in PYTHON_RESERVED_WORDS and parameter.name not in renames:
679
+ spelling = sanitize_identifier(parameter.name)
680
+ if spelling in raw_names:
681
+ raise UnsupportedFeatureError(
682
+ f'export function {declaration.name!r}: parameters {parameter.name!r} and '
683
+ f'{spelling!r} would both be emitted as {spelling!r}', declaration.line)
684
+ renames[parameter.name] = spelling
685
+
686
+ def plan_external_names(self, script: Script) -> None:
687
+ """The spellings an input or an export holds. An input keeps its raw name in the
688
+ tree (codegen spells it, as before, and its settings id is that spelling); a
689
+ keyword-named input whose spelling another input holds was a SyntaxError, so it
690
+ takes a fresh name."""
691
+ input_names = {node.name for node in script.declarations if isinstance(node, InputDecl)}
692
+ for name in sorted(input_names & PYTHON_RESERVED_WORDS):
693
+ spelling = sanitize_identifier(name)
694
+ if spelling in input_names:
695
+ self.moved_inputs[name] = free_spelling(spelling, self.spellings.occupied)
696
+ else:
697
+ self.external_spellings.add(spelling)
698
+ # an input named like a function or method of the script: the input is a parameter of main() and the
699
+ # script's functions are defined inside main(), so the function overwrites the parameter (card #119)
700
+ self.inputs_named_like_a_function = {name for name in input_names
701
+ if name in self.user_function_names or name in self.method_names}
702
+ # an input named like a pynecore name is renamed like any binding (PyneComp: `extend__ren__=input.bool(...)`)
703
+ for name in sorted((input_names & self.library_names) - self.moved_inputs.keys()):
704
+ self.moved_inputs[name] = self.reserved_name(name)
705
+ for name in sorted(self.inputs_named_like_a_function - self.moved_inputs.keys()):
706
+ self.moved_inputs[name] = self.fresh_name(name)
707
+ exported = {node.name for node in script.declarations
708
+ if isinstance(node, FuncDecl) and node.is_export}
709
+ for name in sorted(exported & PYTHON_RESERVED_WORDS):
710
+ spelling = sanitize_identifier(name)
711
+ if spelling in exported or spelling in input_names:
712
+ raise UnsupportedFeatureError(
713
+ f'export function {name!r} would be emitted as {spelling!r}, a name the script '
714
+ f'declares as an input or another export', 0)
715
+ self.external_spellings.add(spelling)
716
+ self.spellings.occupied.update(self.external_spellings)
717
+
718
+ # ---------------------------------------------------------------- expressions
719
+
720
+ def expression(self, node):
721
+ replace = dataclasses.replace
722
+ if node is None or isinstance(node, (str, int, float, bool)):
723
+ return node
724
+ if isinstance(node, list):
725
+ if any(isinstance(item, (Statement, VarDecl, VaripDecl)) for item in node):
726
+ return self.block(node)
727
+ return [self.expression(item) for item in node]
728
+ if isinstance(node, tuple):
729
+ return tuple(self.expression(item) for item in node)
730
+ if isinstance(node, dict):
731
+ return {key: self.expression(value) for key, value in node.items()}
732
+ if isinstance(node, Identifier):
733
+ return replace(node, name=self.resolve_dotted(node.name))
734
+ if isinstance(node, MemberAccess):
735
+ return self.member_access(node)
736
+ if isinstance(node, FunctionCall) and isinstance(node.func, str):
737
+ return self.function_call(node)
738
+ if isinstance(node, MethodCall):
739
+ # `x.t.m(...)` on a call result reaches here with method 't.m'
740
+ head, dot, member = node.method.rpartition('.')
741
+ callee = member if member in self.method_names else None
742
+ method = self.member_path(head) + dot + self.method_spelling(member) if dot else self.method_spelling(member)
743
+ return replace(node, object=self.expression(node.object), method=method,
744
+ args=self.expression(node.args), kwargs=self.keywords(callee, node.kwargs))
745
+ if isinstance(node, IfExpression):
746
+ return replace(node, condition=self.expression(node.condition),
747
+ true_expr=self.block(node.true_expr),
748
+ false_expr=self.block(node.false_expr))
749
+ if isinstance(node, SwitchExpression):
750
+ return replace(node, expr=self.expression(node.expr),
751
+ cases=[(self.expression(case), self.block(body))
752
+ for case, body in node.cases],
753
+ default=self.block(node.default))
754
+ if isinstance(node, SCOPED_STATEMENT_TYPES):
755
+ return self.statement(node)
756
+ if isinstance(node, ASTNode):
757
+ return replace(node, **{field.name: self.expression(getattr(node, field.name))
758
+ for field in dataclasses.fields(node)
759
+ if field.name not in ('line', 'column')})
760
+ return node
761
+
762
+ def function_call(self, node: FunctionCall) -> FunctionCall:
763
+ """A bare name is a function; a dotted one may be a method call on a
764
+ renamed variable, or a namespace call."""
765
+ constructs = False
766
+ if '.' in node.func:
767
+ # `array.new<lib.T>`: the call is one token; its type argument is a type hint (card #111)
768
+ callee_path, bracket, type_argument = node.func.partition('<')
769
+ func = self.resolve_dotted(callee_path, is_call=True) + bracket + self.type_hint(type_argument)
770
+ # `t.m(...)`: only a user method's keywords can name a renamed parameter;
771
+ # `array.get(a, index = 0)` calls the namespace, whatever user method shares the name
772
+ member = node.func.rpartition('.')[2]
773
+ callee = member if member in self.method_names and not self.names_a_namespace(node.func, func) else None
774
+ head, _, constructor = node.func.partition('.')
775
+ constructs = constructor == 'new' and head in self.type_names and self.lookup(head) is None
776
+ else:
777
+ func, callee = self.callee(node.func), node.func
778
+ return dataclasses.replace(node, func=func, args=self.expression(node.args),
779
+ kwargs=self.keywords(callee, node.kwargs, constructs))
780
+
781
+ def member_access(self, node: MemberAccess) -> MemberAccess:
782
+ """`a.b` as a node: the member is a field of a user value (a name that resolves to a user
783
+ binding or declared type, or any computed value), never of a builtin namespace."""
784
+ if not isinstance(node.object, str):
785
+ return dataclasses.replace(node, object=self.expression(node.object),
786
+ member=self.spellings.attribute(node.member))
787
+ head = node.object.partition('.')[0]
788
+ binding = self.lookup(head)
789
+ member = node.member if binding is None and head not in self.type_names else self.spellings.attribute(node.member)
790
+ return dataclasses.replace(node, object=self.resolve_dotted(node.object), member=member)
791
+
792
+ def names_a_namespace(self, pine_func: str, emitted_func: str) -> bool:
793
+ """Whether a dotted call is emitted as a namespace call (`array.get`,
794
+ `chart.point.from_index`, an aliased namespace), not a call on a binding."""
795
+ emitted_head = emitted_func.partition('.')[0]
796
+ if emitted_head in self.namespace_aliases.values():
797
+ return True
798
+ binding = self.lookup(pine_func.partition('.')[0])
799
+ return emitted_head in NAMESPACE_HEADS and (binding is None or binding.emitted != emitted_head)
800
+
801
+ def keywords(self, callee: Optional[str], kwargs: dict, constructs: bool = False) -> dict:
802
+ """Keyword arguments, visited; one that names a renamed parameter of
803
+ the called user function or method `callee` takes the parameter's new name,
804
+ and one of a UDT constructor `T.new(...)` follows the field map."""
805
+ kwargs = self.expression(kwargs)
806
+ if constructs:
807
+ return {self.spellings.attribute(keyword): value for keyword, value in kwargs.items()}
808
+ renames = self.parameter_names.get(callee)
809
+ if not renames:
810
+ return kwargs
811
+ return {renames.get(keyword, keyword): value for keyword, value in kwargs.items()}
812
+
813
+ # ---------------------------------------------------------------- script
814
+
815
+ def global_names(self, script: Script) -> None:
816
+ """Bind every global up front: a function body may read one. Globals bind in source
817
+ order (`global_declarations`), so a value is typed from the globals declared above it."""
818
+ for declaration in global_declarations(script):
819
+ if isinstance(declaration, (VarDecl, VaripDecl)):
820
+ self.declare(declaration.name, self.value_type(declaration.type_hint, declaration.value))
821
+ elif isinstance(declaration, InputDecl):
822
+ binding = self.keep(declaration.name, input_type(declaration))
823
+ if declaration.name in self.moved_inputs:
824
+ binding = dataclasses.replace(binding, emitted=self.moved_inputs[declaration.name])
825
+ self.scopes[0][declaration.name] = binding
826
+ elif isinstance(declaration.target, TupleDestructure):
827
+ for name in declaration.target.names:
828
+ self.declare(name)
829
+ else:
830
+ self.declare(declaration.target, self.value_type(declaration.type_hint, declaration.value))
831
+
832
+ def global_declaration(self, declaration):
833
+ replace = dataclasses.replace
834
+ if isinstance(declaration, FuncDecl):
835
+ return self.function(declaration)
836
+ if isinstance(declaration, (VarDecl, VaripDecl)):
837
+ return replace(declaration, name=self.resolve(declaration.name),
838
+ type_hint=self.type_hint(declaration.type_hint),
839
+ value=self.own_value(declaration, [declaration.name]))
840
+ if isinstance(declaration, InputDecl):
841
+ return self.input_declaration(declaration)
842
+ if isinstance(declaration, TypeDecl):
843
+ return self.type_declaration(declaration)
844
+ if isinstance(declaration, EnumDecl):
845
+ return self.enum_declaration(declaration)
846
+ return declaration
847
+
848
+ def input_declaration(self, declaration: InputDecl) -> InputDecl:
849
+ """An input keeps its name. Its arguments are read when the module loads, before the
850
+ input exists, so its own name there is the builtin of that name (card #86): a builtin
851
+ series, which the import resolver does not import for the input's reads, is read by
852
+ the namespace alias."""
853
+ name = declaration.name
854
+ saved = self.read_builtin_by_alias({name} & self.builtin_globals.keys(),
855
+ [declaration.args, declaration.kwargs])
856
+ arguments = self.expression(declaration.args), self.expression(declaration.kwargs)
857
+ self.scopes[0].update(saved)
858
+ args, kwargs = arguments
859
+ if name in self.inputs_named_like_a_function and len(args) < 2 and 'title' not in kwargs:
860
+ # Pine shows the variable name when the call gives no title: the rename must not change the label
861
+ kwargs = {**kwargs, 'title': Literal(value=name, literal_type='string', is_double_quoted=True)}
862
+ return dataclasses.replace(declaration, name=self.moved_inputs.get(name, name), args=args, kwargs=kwargs)
863
+
864
+ def read_builtin_by_alias(self, names: Set[str], tree) -> Dict[str, Binding]:
865
+ """Until the caller restores the returned bindings, each input of `names` that `tree`
866
+ reads stands for the builtin of its name, spelled by the namespace alias (card #86)."""
867
+ saved = {}
868
+ for name in sorted(names):
869
+ if any(isinstance(node, Identifier) and node.name.partition('.')[0] == name for node in walk(tree)):
870
+ saved[name] = self.scopes[0][name]
871
+ self.scopes[0][name] = Binding(self.namespace(name))
872
+ self.moved_bindings = True
873
+ return saved
874
+
875
+ def own_value(self, declaration, names) -> Any:
876
+ """The value of a global's declaration. The global is declared once its declaration ends,
877
+ so where this is the first declaration of a name like a builtin series, the value's read
878
+ of that name is the builtin (card #89): `close = close * 2`."""
879
+ first = {name for name in names if self.builtin_globals.get(name) is declaration}
880
+ saved = self.read_builtin_by_alias(first, declaration.value)
881
+ value = self.expression(declaration.value)
882
+ self.scopes[0].update(saved)
883
+ return value
884
+
885
+ def in_source_order(self, visit, item):
886
+ """Visit a top-level item; a global named like a builtin series and declared after it is
887
+ not visible there yet, so the item's reads of that name are the builtin."""
888
+ declared_later = {name for name, declaration in self.builtin_globals.items()
889
+ if 0 < item.line < declaration.line}
890
+ saved = self.read_builtin_by_alias(declared_later, item)
891
+ result = visit(item)
892
+ self.scopes[0].update(saved)
893
+ return result
894
+
895
+ def type_declaration(self, declaration: TypeDecl) -> TypeDecl:
896
+ """A UDT's name and fields in their emitted spelling; a field's type may name a keyword-named UDT."""
897
+ fields = [(self.spellings.attribute(field[0]), self.type_hint(field[1]), *self.field_default(field[2:]))
898
+ for field in declaration.fields]
899
+ return dataclasses.replace(declaration, name=self.type_spelling(declaration.name), fields=fields)
900
+
901
+ def field_default(self, default_part: tuple) -> tuple:
902
+ """A field's default, read as it stands in the module: a dotted constant headed by a renamed library alias
903
+ (`lib.Mode.a`) starts with the alias's emitted name (card #111). Every other default is left as written: a
904
+ built-in series (`close`) stays the built-in, whatever a later global or input of its name does."""
905
+ if default_part and isinstance(default_part[0], Identifier):
906
+ default = default_part[0]
907
+ head, dot, _ = default.name.partition('.')
908
+ if dot and head in self.library_alias_spellings:
909
+ return (dataclasses.replace(default, name=self.resolve_dotted(default.name)), *default_part[1:])
910
+ return default_part
911
+
912
+ def enum_declaration(self, declaration: EnumDecl) -> EnumDecl:
913
+ """An enum's name and members in their emitted spelling. A member without a value
914
+ stands for its own name as a string; it keeps that value when its attribute moves."""
915
+ members = []
916
+ for member in declaration.members:
917
+ if isinstance(member, tuple):
918
+ members.append((self.spellings.attribute(member[0]), *member[1:]))
919
+ elif self.spellings.attribute(member) != member:
920
+ members.append((self.spellings.attribute(member),
921
+ Literal(value=member, literal_type='string', is_double_quoted=True)))
922
+ else:
923
+ members.append(member)
924
+ return dataclasses.replace(declaration, name=self.type_spelling(declaration.name), members=members)
925
+
926
+ def global_statement(self, statement):
927
+ """A top-level assignment declares a global, already bound by global_names."""
928
+ if not isinstance(statement, Assignment):
929
+ return self.statement(statement)
930
+ target = statement.target
931
+ value = self.own_value(statement, target.names if isinstance(target, TupleDestructure) else [target])
932
+ if isinstance(statement.target, TupleDestructure):
933
+ names = [self.resolve(name) for name in statement.target.names]
934
+ return dataclasses.replace(statement, value=value, target=dataclasses.replace(
935
+ statement.target, names=names, value=value))
936
+ return dataclasses.replace(statement, value=value, target=self.resolve(statement.target),
937
+ type_hint=self.type_hint(statement.type_hint))
938
+
939
+ def library_aliases(self, script: Script) -> Dict[str, str]:
940
+ """Bind every `import user/lib/version as alias` whose alias (the library name without one) the compiled
941
+ module binds itself to a fresh name: alias -> its emitted name (card #111). Every use of the alias then
942
+ resolves like any renamed global."""
943
+ for imported in script.imports:
944
+ name = imported.alias or imported.library
945
+ if name in LIBRARY_ALIAS_RENAMED_NAMES and name not in self.library_alias_spellings:
946
+ self.library_alias_spellings[name] = self.fresh_name(name)
947
+ self.scopes[0][name] = Binding(emitted=self.library_alias_spellings[name])
948
+ return self.library_alias_spellings
949
+
950
+ def run(self, script: Script) -> Script:
951
+ self.scopes = [{}]
952
+ self.scope_declarations = [set()]
953
+ renamed_aliases = self.library_aliases(script)
954
+ self.global_names(script)
955
+ declarations = [self.in_source_order(self.global_declaration, declaration)
956
+ for declaration in script.declarations]
957
+ body = [self.in_source_order(self.global_statement, statement) for statement in script.body]
958
+ if not (self.shadowed_names or self.library_names or any(self.parameter_names.values())
959
+ or self.spellings.changed or self.moved_bindings or self.moved_inputs or self.function_names
960
+ or renamed_aliases):
961
+ return script
962
+ imports = [dataclasses.replace(imported, alias=renamed_aliases[imported.alias or imported.library])
963
+ if (imported.alias or imported.library) in renamed_aliases else imported
964
+ for imported in script.imports]
965
+ return dataclasses.replace(script, declarations=declarations, body=body, imports=imports)