pine2pyne 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pine2pyne/__init__.py +84 -0
- pine2pyne/__main__.py +12 -0
- pine2pyne/ast_nodes.py +405 -0
- pine2pyne/ast_walk.py +93 -0
- pine2pyne/block_value_lowering.py +694 -0
- pine2pyne/bool_builtins.py +42 -0
- pine2pyne/bool_value_typing.py +580 -0
- pine2pyne/cli.py +247 -0
- pine2pyne/codegen.py +886 -0
- pine2pyne/compat/__init__.py +19 -0
- pine2pyne/compat/__main__.py +6 -0
- pine2pyne/compat/cli.py +114 -0
- pine2pyne/compat/comparison.py +105 -0
- pine2pyne/compat/corpus.py +73 -0
- pine2pyne/compat/report.py +95 -0
- pine2pyne/compat/runner.py +63 -0
- pine2pyne/condition_typing.py +260 -0
- pine2pyne/conditional_history_calls.py +96 -0
- pine2pyne/core/__init__.py +26 -0
- pine2pyne/core/contracts.py +125 -0
- pine2pyne/di.py +173 -0
- pine2pyne/emit/__init__.py +5 -0
- pine2pyne/emit/legacy.py +14 -0
- pine2pyne/emit/pynecomp/__init__.py +20 -0
- pine2pyne/emit/pynecomp/banner.py +30 -0
- pine2pyne/emit/pynecomp/blank_lines.py +69 -0
- pine2pyne/emit/pynecomp/emitter.py +61 -0
- pine2pyne/emit/pynecomp/imports.py +65 -0
- pine2pyne/emit/pynecomp/passes/__init__.py +84 -0
- pine2pyne/emit/pynecomp/passes/annotations.py +422 -0
- pine2pyne/emit/pynecomp/passes/base.py +69 -0
- pine2pyne/emit/pynecomp/passes/builtins.py +169 -0
- pine2pyne/emit/pynecomp/passes/hoisting.py +205 -0
- pine2pyne/emit/pynecomp/passes/imports.py +112 -0
- pine2pyne/emit/pynecomp/passes/legacy_colors.py +101 -0
- pine2pyne/emit/pynecomp/passes/library.py +205 -0
- pine2pyne/emit/pynecomp/passes/rewrites.py +215 -0
- pine2pyne/emit/pynecomp/printer.py +288 -0
- pine2pyne/emit/pynecomp/unparser.py +186 -0
- pine2pyne/emitted_names.py +152 -0
- pine2pyne/errors.py +69 -0
- pine2pyne/for_loop_end_bound.py +172 -0
- pine2pyne/grouped_history_lowering.py +310 -0
- pine2pyne/import_resolver.py +469 -0
- pine2pyne/input_hoisting.py +195 -0
- pine2pyne/legacy_function_names.py +74 -0
- pine2pyne/legacy_sessions.py +68 -0
- pine2pyne/lexer.py +630 -0
- pine2pyne/library_collision.py +434 -0
- pine2pyne/namespace_members.py +247 -0
- pine2pyne/node_ids.py +235 -0
- pine2pyne/once_lowering.py +75 -0
- pine2pyne/overload_resolution.py +475 -0
- pine2pyne/parse_repair.py +398 -0
- pine2pyne/parser.py +1873 -0
- pine2pyne/pine_builtins.py +394 -0
- pine2pyne/pine_semantics.py +604 -0
- pine2pyne/pipeline/__init__.py +18 -0
- pine2pyne/pipeline/service.py +70 -0
- pine2pyne/pipeline/stages.py +73 -0
- pine2pyne/scope_renamer.py +965 -0
- pine2pyne/strategy_when_lowering.py +101 -0
- pine2pyne/strict_boolean.py +101 -0
- pine2pyne/symbol_table.py +166 -0
- pine2pyne/tokens.py +143 -0
- pine2pyne/transformer.py +2510 -0
- pine2pyne/type_inference.py +422 -0
- pine2pyne/udt_field_defaults.py +149 -0
- pine2pyne/value_blocks.py +357 -0
- pine2pyne-0.2.4.dist-info/METADATA +99 -0
- pine2pyne-0.2.4.dist-info/RECORD +75 -0
- pine2pyne-0.2.4.dist-info/WHEEL +5 -0
- pine2pyne-0.2.4.dist-info/entry_points.txt +3 -0
- pine2pyne-0.2.4.dist-info/licenses/LICENSE +201 -0
- pine2pyne-0.2.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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)
|