@pineforge/codegen-pyodide 0.10.4 → 1.0.0-rc.1
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.
- package/README.md +16 -16
- package/glue.py +24 -16
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +125 -34
- package/pineforge_codegen/analyzer/__init__.py +2 -0
- package/pineforge_codegen/analyzer/base.py +754 -76
- package/pineforge_codegen/analyzer/call_handlers.py +260 -40
- package/pineforge_codegen/analyzer/contracts.py +37 -0
- package/pineforge_codegen/analyzer/diagnostics.py +30 -4
- package/pineforge_codegen/analyzer/tables.py +49 -8
- package/pineforge_codegen/analyzer/types.py +33 -1
- package/pineforge_codegen/ast_nodes.py +32 -1
- package/pineforge_codegen/block_locals.py +185 -0
- package/pineforge_codegen/builtin_keywords.py +42 -0
- package/pineforge_codegen/codegen/base.py +896 -156
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +946 -213
- package/pineforge_codegen/codegen/helpers.py +435 -14
- package/pineforge_codegen/codegen/host_members.py +162 -0
- package/pineforge_codegen/codegen/input.py +252 -85
- package/pineforge_codegen/codegen/security.py +4372 -377
- package/pineforge_codegen/codegen/session_market.py +71 -0
- package/pineforge_codegen/codegen/ta.py +1188 -100
- package/pineforge_codegen/codegen/tables.py +193 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +1882 -78
- package/pineforge_codegen/codegen/visit_call.py +920 -131
- package/pineforge_codegen/codegen/visit_expr.py +738 -57
- package/pineforge_codegen/codegen/visit_stmt.py +595 -49
- package/pineforge_codegen/external_requests.py +877 -0
- package/pineforge_codegen/lexer.py +104 -22
- package/pineforge_codegen/library_inline.py +1304 -0
- package/pineforge_codegen/library_modules.py +126 -0
- package/pineforge_codegen/library_v5.py +683 -0
- package/pineforge_codegen/limits.py +138 -0
- package/pineforge_codegen/method_binding.py +33 -0
- package/pineforge_codegen/parser.py +384 -68
- package/pineforge_codegen/pine_libraries.py +266 -0
- package/pineforge_codegen/pine_spelling.py +216 -0
- package/pineforge_codegen/pragmas.py +64 -10
- package/pineforge_codegen/security_contexts.py +1585 -0
- package/pineforge_codegen/session_reads.py +84 -0
- package/pineforge_codegen/signatures.py +48 -23
- package/pineforge_codegen/support_checker.py +1106 -85
- package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +23 -21
- package/transpile.worker.mjs +24 -16
- package/pineforge_codegen-0.10.4.tar.gz +0 -0
|
@@ -0,0 +1,1304 @@
|
|
|
1
|
+
"""Inline the Pine libraries a script imports.
|
|
2
|
+
|
|
3
|
+
TradingView links a published library into every script that imports it;
|
|
4
|
+
PineForge has no linker, so ``transpile()`` inlines each library's source
|
|
5
|
+
into the importing script before the support check, and the rest of the
|
|
6
|
+
pipeline sees one program:
|
|
7
|
+
|
|
8
|
+
- Resolution (``pine_libraries``): the source of every import the script
|
|
9
|
+
uses, from ``libraries=`` or the probe's own requests manifest.
|
|
10
|
+
- Modules (``library_modules``): each library parsed on its own, with its own
|
|
11
|
+
``//@version``, following its own imports.
|
|
12
|
+
- Reachability: only the exports the script reaches, and what they reach in
|
|
13
|
+
turn, are inlined. Library example code (plots, inputs) and every unused
|
|
14
|
+
export are dropped, so an unused export PineForge cannot lower does not
|
|
15
|
+
refuse the script.
|
|
16
|
+
- Hygiene: every inlined top-level name gets a module-qualified name, and so
|
|
17
|
+
does every local and parameter of an inlined function or method; an
|
|
18
|
+
importing script and several libraries can then share any spelling. A
|
|
19
|
+
keyword argument follows its parameter's new name.
|
|
20
|
+
- ``alias.f(...)``, ``alias.T.new(...)``, ``alias.E.member``, ``alias.C`` and
|
|
21
|
+
``alias.T`` in a type resolve into the library; an exported method is
|
|
22
|
+
called with method syntax and keeps its name (methods bind by receiver
|
|
23
|
+
type). An alias equal to a built-in namespace (``import TradingView/ta/7``
|
|
24
|
+
has none, so it is ``ta``) reads a member the built-in namespace has as the
|
|
25
|
+
built-in, as TradingView's ``usedLibs`` shows, and any other member from
|
|
26
|
+
the library.
|
|
27
|
+
- An inlined function is an ordinary user function of the program, so its
|
|
28
|
+
state is per call site exactly as a user function's.
|
|
29
|
+
- A v5 library is lowered with v5 rules (``library_v5``).
|
|
30
|
+
|
|
31
|
+
With no library sources configured the program is returned unchanged: every
|
|
32
|
+
import keeps the refusal it has today.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import re
|
|
38
|
+
from collections.abc import Mapping
|
|
39
|
+
|
|
40
|
+
from .ast_nodes import (
|
|
41
|
+
ASTNode, ArgOrder, Assignment, BinOp, BoolLiteral, BreakStmt, ColorLiteral,
|
|
42
|
+
ContinueStmt, EnumDecl, ExprStmt, ForInStmt, ForStmt, FuncCall, FuncDef,
|
|
43
|
+
Identifier, IfStmt, ImportStmt, MemberAccess, MethodDef, NaLiteral,
|
|
44
|
+
NumberLiteral, Program, StrategyDecl, StringLiteral, Subscript, SwitchStmt,
|
|
45
|
+
Ternary, TupleAssign, TupleLiteral, TypeAnnotation, TypeDecl, TypeField,
|
|
46
|
+
UnaryOp, VarDecl, WhileStmt,
|
|
47
|
+
)
|
|
48
|
+
from .errors import CompileError, Diagnostic, Level, Phase, SourceLocation
|
|
49
|
+
from .library_modules import LibraryModule, parse_library_module
|
|
50
|
+
from .limits import TimeBudget
|
|
51
|
+
from .pine_libraries import LibraryResolveError, LibraryResolver, library_resolver
|
|
52
|
+
from .support_checker import (
|
|
53
|
+
BUILTIN_NAMESPACE_IMPORT_MEMBERS,
|
|
54
|
+
import_is_builtin_namespace_no_op,
|
|
55
|
+
import_spelling,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Built-in namespaces. An import alias equal to ``ta``, ``math`` or ``str``
|
|
59
|
+
# reads the namespace's built-in members as built-ins; an alias equal to any
|
|
60
|
+
# other built-in namespace is refused (the member split is unknown there).
|
|
61
|
+
_BUILTIN_NAMESPACES = frozenset({
|
|
62
|
+
"ta", "math", "str", "array", "map", "matrix", "color", "input",
|
|
63
|
+
"request", "strategy", "syminfo", "timeframe", "barstate", "session",
|
|
64
|
+
"chart", "label", "line", "box", "table", "linefill", "polyline", "log",
|
|
65
|
+
"runtime", "ticker", "dividends", "earnings", "splits", "currency",
|
|
66
|
+
"display", "extend", "format", "hline", "location", "order", "plot",
|
|
67
|
+
"position", "scale", "shape", "size", "text", "xloc", "yloc", "alert",
|
|
68
|
+
"barmerge", "font", "adjustment", "backadjustment", "settlement_as_close",
|
|
69
|
+
"dayofweek", "footprint", "volume_row", "timenow", "time",
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
_TYPE_TOKEN_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*")
|
|
73
|
+
|
|
74
|
+
# Statement nodes that open a block scope.
|
|
75
|
+
_BLOCK_STATEMENTS = (IfStmt, ForStmt, ForInStmt, WhileStmt, SwitchStmt)
|
|
76
|
+
|
|
77
|
+
# Pine's qualifiers, weakest first: an argument fits a parameter whose
|
|
78
|
+
# qualifier is at least its own. An unqualified library parameter takes a
|
|
79
|
+
# series argument.
|
|
80
|
+
_CONST, _SIMPLE, _SERIES = 0, 1, 2
|
|
81
|
+
_QUALIFIER_RANKS = {"const": _CONST, "simple": _SIMPLE, "series": _SERIES}
|
|
82
|
+
_LITERALS = (NumberLiteral, StringLiteral, BoolLiteral, NaLiteral, ColorLiteral)
|
|
83
|
+
# Members fixed for the run (``syminfo.*``, ``timeframe.period``), and
|
|
84
|
+
# namespaces of constants.
|
|
85
|
+
_SIMPLE_MEMBER_NAMESPACES = frozenset({"syminfo", "timeframe"})
|
|
86
|
+
_CONST_MEMBER_NAMESPACES = frozenset({
|
|
87
|
+
"barmerge", "color", "currency", "display", "extend", "font", "format",
|
|
88
|
+
"location", "order", "position", "scale", "shape", "size", "text", "xloc",
|
|
89
|
+
"yloc", "adjustment", "backadjustment", "settlement_as_close",
|
|
90
|
+
})
|
|
91
|
+
# Calls whose result is as fixed as their arguments.
|
|
92
|
+
_PURE_CALL_NAMESPACES = frozenset({"str", "ticker"})
|
|
93
|
+
_PURE_CALLS = frozenset({"int", "float", "bool", "string", "color", "na", "nz"})
|
|
94
|
+
_PURE_TIMEFRAME_CALLS = frozenset({"in_seconds", "from_seconds"})
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _error(message: str, loc: SourceLocation | None, filename: str,
|
|
98
|
+
hint: str | None = None) -> Diagnostic:
|
|
99
|
+
return Diagnostic(
|
|
100
|
+
level=Level.ERROR, phase=Phase.PARSER,
|
|
101
|
+
location=loc or SourceLocation(file=filename, line=1, col=1, end_col=1),
|
|
102
|
+
message=message, hint=hint,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _callee_alias(node) -> tuple[str, str] | None:
|
|
107
|
+
"""``alias.member`` as (alias, member) when ``node`` spells one."""
|
|
108
|
+
if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
|
|
109
|
+
return node.object.name, node.member
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class _LinkError(Exception):
|
|
114
|
+
def __init__(self, diagnostic: Diagnostic) -> None:
|
|
115
|
+
super().__init__(diagnostic.message)
|
|
116
|
+
self.diagnostic = diagnostic
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class _Module:
|
|
120
|
+
"""One loaded library and the names it is inlined under."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, lib: LibraryModule, prefix: str) -> None:
|
|
123
|
+
self.lib = lib
|
|
124
|
+
self.prefix = prefix
|
|
125
|
+
self.names: dict[str, str] = {} # top-level name -> new name
|
|
126
|
+
self.method_names: dict[str, str] = {} # method name -> emitted name
|
|
127
|
+
self.targets: dict[str, str] = {} # own alias -> module path
|
|
128
|
+
self.included: dict[int, object] = {} # reachable top-level nodes
|
|
129
|
+
self.deps: list[str] = [] # modules its code references
|
|
130
|
+
# An overload after the first of an overloaded function: its
|
|
131
|
+
# callable name (its locals' prefix) and new name, by id().
|
|
132
|
+
self.overloads: dict[int, tuple[str, str]] = {}
|
|
133
|
+
# Each function's and method's parameters as the library spells them,
|
|
134
|
+
# by id(): processing renames ``params`` in place, and a keyword
|
|
135
|
+
# argument binds by the spelled name.
|
|
136
|
+
self.params: dict[int, tuple[str, ...]] = {}
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def path(self) -> str:
|
|
140
|
+
return self.lib.path
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class _Linker:
|
|
144
|
+
def __init__(self, program: Program, resolver: LibraryResolver, filename: str,
|
|
145
|
+
budget: TimeBudget | None) -> None:
|
|
146
|
+
self._program = program
|
|
147
|
+
self._resolver = resolver
|
|
148
|
+
self._filename = filename
|
|
149
|
+
self._budget = budget
|
|
150
|
+
self._modules: dict[str, _Module] = {}
|
|
151
|
+
self._load_order: list[str] = []
|
|
152
|
+
self._queue: list[tuple[_Module, object]] = []
|
|
153
|
+
self._allocated: set[str] = set()
|
|
154
|
+
self._taken: set[str] = set()
|
|
155
|
+
self._local_names: dict[tuple[str, str, str], str] = {}
|
|
156
|
+
self._main_targets: dict[str, str] = {} # main alias -> module path
|
|
157
|
+
self._main_deps: list[str] = []
|
|
158
|
+
self._main_methods: set[str] = set()
|
|
159
|
+
# The overload each call of an overloaded function binds to, by id().
|
|
160
|
+
self._overload_of: dict[int, FuncDef] = {}
|
|
161
|
+
# Qualifier ranks of inlined parameters, by new name.
|
|
162
|
+
self._param_ranks: dict[str, int] = {}
|
|
163
|
+
self._main_fixed: dict[str, object] | None = None
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
# Names
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
def _collect_names(self, root) -> None:
|
|
170
|
+
stack = [root]
|
|
171
|
+
while stack:
|
|
172
|
+
node = stack.pop()
|
|
173
|
+
if isinstance(node, (list, tuple)):
|
|
174
|
+
stack.extend(node)
|
|
175
|
+
continue
|
|
176
|
+
if isinstance(node, dict):
|
|
177
|
+
stack.extend(node.values())
|
|
178
|
+
continue
|
|
179
|
+
if isinstance(node, TypeField):
|
|
180
|
+
self._taken.add(node.name)
|
|
181
|
+
self._taken.update(_TYPE_TOKEN_RE.findall(node.type_name or ""))
|
|
182
|
+
stack.append(node.default)
|
|
183
|
+
continue
|
|
184
|
+
if not isinstance(node, ASTNode):
|
|
185
|
+
continue
|
|
186
|
+
for key, value in vars(node).items():
|
|
187
|
+
if key == "loc":
|
|
188
|
+
continue
|
|
189
|
+
if key == "annotations":
|
|
190
|
+
notes = value or {}
|
|
191
|
+
for hint in notes.get("param_type_hints") or ():
|
|
192
|
+
if isinstance(hint, str):
|
|
193
|
+
self._taken.update(_TYPE_TOKEN_RE.findall(hint))
|
|
194
|
+
stack.extend(d for d in notes.get("param_defaults") or () if d is not None)
|
|
195
|
+
continue
|
|
196
|
+
if isinstance(value, str):
|
|
197
|
+
if key in ("name", "var", "type_hint", "type_name", "member"):
|
|
198
|
+
self._taken.update(_TYPE_TOKEN_RE.findall(value))
|
|
199
|
+
continue
|
|
200
|
+
if key in ("params", "names", "vars") and isinstance(value, list):
|
|
201
|
+
self._taken.update(v for v in value if isinstance(v, str))
|
|
202
|
+
continue
|
|
203
|
+
stack.append(value)
|
|
204
|
+
|
|
205
|
+
def _alloc(self, base: str) -> str:
|
|
206
|
+
name = base
|
|
207
|
+
n = 2
|
|
208
|
+
while name in self._allocated or name in self._taken:
|
|
209
|
+
name = f"{base}_{n}"
|
|
210
|
+
n += 1
|
|
211
|
+
self._allocated.add(name)
|
|
212
|
+
return name
|
|
213
|
+
|
|
214
|
+
def _local_name(self, mod: _Module, callable_name: str, name: str) -> str:
|
|
215
|
+
key = (mod.path, callable_name, name)
|
|
216
|
+
if key not in self._local_names:
|
|
217
|
+
self._local_names[key] = self._alloc(f"{mod.prefix}{callable_name}__{name}")
|
|
218
|
+
return self._local_names[key]
|
|
219
|
+
|
|
220
|
+
def _prefix(self, lib: LibraryModule) -> str:
|
|
221
|
+
base = f"{lib.name}_v{lib.path.rsplit('/', 1)[1]}"
|
|
222
|
+
prefix = f"{base}__"
|
|
223
|
+
n = 2
|
|
224
|
+
taken_prefixes = {m.prefix for m in self._modules.values()}
|
|
225
|
+
while prefix in taken_prefixes or any(t.startswith(prefix) for t in self._taken):
|
|
226
|
+
prefix = f"{base}_{n}__"
|
|
227
|
+
n += 1
|
|
228
|
+
return prefix
|
|
229
|
+
|
|
230
|
+
# ------------------------------------------------------------------
|
|
231
|
+
# Loading
|
|
232
|
+
# ------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def _load(self, path: str, at: ASTNode) -> _Module:
|
|
235
|
+
mod = self._modules.get(path)
|
|
236
|
+
if mod is not None:
|
|
237
|
+
return mod
|
|
238
|
+
try:
|
|
239
|
+
source = self._resolver.load(path)
|
|
240
|
+
except LibraryResolveError as exc:
|
|
241
|
+
raise _LinkError(_error(
|
|
242
|
+
f"Import is not supported: '{path}': {exc}", at.loc, self._filename))
|
|
243
|
+
lib = parse_library_module(path, source.text, budget=self._budget)
|
|
244
|
+
before = set(self._taken)
|
|
245
|
+
self._collect_names(lib.program)
|
|
246
|
+
clash = sorted((self._taken - before) & self._allocated)
|
|
247
|
+
if clash:
|
|
248
|
+
raise _LinkError(_error(
|
|
249
|
+
f"library '{path}' spells '{clash[0]}', a name PineForge gave "
|
|
250
|
+
"to an inlined definition", at.loc, self._filename))
|
|
251
|
+
mod = _Module(lib, self._prefix(lib))
|
|
252
|
+
self._modules[path] = mod
|
|
253
|
+
for stmt in lib.program.body:
|
|
254
|
+
if isinstance(stmt, (FuncDef, MethodDef)):
|
|
255
|
+
mod.params[id(stmt)] = tuple(stmt.params)
|
|
256
|
+
self._load_order.append(path)
|
|
257
|
+
for alias, stmt in lib.imports.items():
|
|
258
|
+
mod.targets[alias] = stmt.path
|
|
259
|
+
# Top-level names, in source order (deterministic allocation).
|
|
260
|
+
for stmt in lib.program.body:
|
|
261
|
+
if isinstance(stmt, (FuncDef, TypeDecl, EnumDecl)):
|
|
262
|
+
if stmt.name not in mod.names:
|
|
263
|
+
mod.names[stmt.name] = self._alloc(mod.prefix + stmt.name)
|
|
264
|
+
elif isinstance(stmt, VarDecl):
|
|
265
|
+
if stmt.name not in mod.names:
|
|
266
|
+
mod.names[stmt.name] = self._alloc(mod.prefix + stmt.name)
|
|
267
|
+
elif isinstance(stmt, TupleAssign):
|
|
268
|
+
for name in stmt.names:
|
|
269
|
+
if name != "_" and name not in mod.names:
|
|
270
|
+
mod.names[name] = self._alloc(mod.prefix + name)
|
|
271
|
+
elif isinstance(stmt, MethodDef):
|
|
272
|
+
if stmt.name not in mod.method_names:
|
|
273
|
+
exported = any(lib.exported(m) for m in lib.methods[stmt.name])
|
|
274
|
+
mod.method_names[stmt.name] = (
|
|
275
|
+
stmt.name if exported else self._alloc(mod.prefix + stmt.name))
|
|
276
|
+
return mod
|
|
277
|
+
|
|
278
|
+
def _module_of(self, owner: _Module | None, alias: str, at: ASTNode) -> _Module:
|
|
279
|
+
"""The module ``alias`` names in ``owner`` (None: the main script)."""
|
|
280
|
+
targets = self._main_targets if owner is None else owner.targets
|
|
281
|
+
path = targets[alias]
|
|
282
|
+
mod = self._load(path, at)
|
|
283
|
+
deps = self._main_deps if owner is None else owner.deps
|
|
284
|
+
if path not in deps:
|
|
285
|
+
deps.append(path)
|
|
286
|
+
return mod
|
|
287
|
+
|
|
288
|
+
# ------------------------------------------------------------------
|
|
289
|
+
# Reachability
|
|
290
|
+
# ------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
def _include(self, mod: _Module, node) -> None:
|
|
293
|
+
if id(node) not in mod.included:
|
|
294
|
+
mod.included[id(node)] = node
|
|
295
|
+
self._queue.append((mod, node))
|
|
296
|
+
|
|
297
|
+
def _include_function(self, mod: _Module, name: str, at: ASTNode,
|
|
298
|
+
site: tuple | None = None) -> str:
|
|
299
|
+
"""The new name of ``name`` as the call ``at`` reaches it; ``site``
|
|
300
|
+
is the call's ``(module, scope)`` (module None: the script)."""
|
|
301
|
+
defs = mod.lib.functions[name]
|
|
302
|
+
if len(defs) == 1:
|
|
303
|
+
self._include(mod, defs[0])
|
|
304
|
+
return mod.names[name]
|
|
305
|
+
chosen = self._overload(mod, name, defs, at, site)
|
|
306
|
+
self._overload_of[id(at)] = chosen
|
|
307
|
+
self._include(mod, chosen)
|
|
308
|
+
return self._overload_names(mod, name, chosen)[1]
|
|
309
|
+
|
|
310
|
+
def _overload_names(self, mod: _Module, name: str, fdef: FuncDef) -> tuple[str, str]:
|
|
311
|
+
"""(callable name, new name) of one of ``name``'s overloads: the first
|
|
312
|
+
keeps the function's, each later one gets its own."""
|
|
313
|
+
index = next(i for i, d in enumerate(mod.lib.functions[name]) if d is fdef)
|
|
314
|
+
if index == 0:
|
|
315
|
+
return name, mod.names[name]
|
|
316
|
+
if id(fdef) not in mod.overloads:
|
|
317
|
+
callable_name = f"{name}_{index + 1}"
|
|
318
|
+
mod.overloads[id(fdef)] = (callable_name,
|
|
319
|
+
self._alloc(mod.prefix + callable_name))
|
|
320
|
+
return mod.overloads[id(fdef)]
|
|
321
|
+
|
|
322
|
+
def _callable_of(self, mod: _Module, name: str, call: ASTNode) -> tuple[str, set[str]]:
|
|
323
|
+
"""The callable name and parameters a call's keywords bind to."""
|
|
324
|
+
chosen = self._overload_of.get(id(call))
|
|
325
|
+
if chosen is None:
|
|
326
|
+
return name, self._function_params(mod, name)
|
|
327
|
+
return (self._overload_names(mod, name, chosen)[0],
|
|
328
|
+
set(mod.params.get(id(chosen), chosen.params)))
|
|
329
|
+
|
|
330
|
+
def _overload(self, mod: _Module, name: str, defs: list, at: ASTNode,
|
|
331
|
+
site: tuple | None) -> FuncDef:
|
|
332
|
+
"""The overload a call binds to, as TradingView picks it: the
|
|
333
|
+
overloads its arguments bind to by count and keyword; of those that
|
|
334
|
+
differ by their parameters' qualifiers alone, the one whose
|
|
335
|
+
qualifiers are the weakest its arguments fit (TradingView/Request/3
|
|
336
|
+
overloads ``simple string`` parameters with ``series string`` ones:
|
|
337
|
+
an input's or a literal's call binds to the ``simple`` one, a series
|
|
338
|
+
argument to the ``series`` one). Overloads that differ by type are
|
|
339
|
+
refused by name, as every overload was."""
|
|
340
|
+
refusal = _error(
|
|
341
|
+
f"library '{mod.path}' defines '{name}' {len(defs)} times "
|
|
342
|
+
"(overloads); PineForge does not inline an overloaded library "
|
|
343
|
+
"function", at.loc, self._filename)
|
|
344
|
+
if not isinstance(at, FuncCall) or site is None:
|
|
345
|
+
raise _LinkError(refusal)
|
|
346
|
+
bound = {d_id: args for d_id, args in
|
|
347
|
+
((id(d), self._bind_arguments(mod, d, at)) for d in defs) if args is not None}
|
|
348
|
+
fits = [d for d in defs if id(d) in bound]
|
|
349
|
+
if len(fits) == 1:
|
|
350
|
+
return fits[0]
|
|
351
|
+
if not fits:
|
|
352
|
+
raise _LinkError(_error(
|
|
353
|
+
f"no overload of '{name}' of library '{mod.path}' takes the "
|
|
354
|
+
"arguments of this call", at.loc, self._filename))
|
|
355
|
+
hints = {tuple((d.annotations or {}).get("param_type_hints") or ()) for d in fits}
|
|
356
|
+
if len(hints) > 1:
|
|
357
|
+
raise _LinkError(refusal)
|
|
358
|
+
best: list[tuple[int, FuncDef]] = []
|
|
359
|
+
for fdef in fits:
|
|
360
|
+
qualifiers = list((fdef.annotations or {}).get("param_qualifiers") or ())
|
|
361
|
+
ranks = [_QUALIFIER_RANKS.get(q, _SERIES) for q in qualifiers]
|
|
362
|
+
ranks += [_SERIES] * (len(fdef.params) - len(ranks))
|
|
363
|
+
if all(self._arg_rank(arg, site) <= ranks[i] for i, arg in bound[id(fdef)].items()):
|
|
364
|
+
best.append((sum(ranks), fdef))
|
|
365
|
+
best.sort(key=lambda item: item[0])
|
|
366
|
+
if not best or (len(best) > 1 and best[0][0] == best[1][0]):
|
|
367
|
+
raise _LinkError(refusal)
|
|
368
|
+
return best[0][1]
|
|
369
|
+
|
|
370
|
+
@staticmethod
|
|
371
|
+
def _bind_arguments(mod: _Module, fdef: FuncDef, call: FuncCall) -> dict[int, ASTNode] | None:
|
|
372
|
+
"""``call``'s arguments by ``fdef``'s parameter index, or None when
|
|
373
|
+
they do not bind (too many, an unknown keyword, one given twice, a
|
|
374
|
+
parameter without a default left out)."""
|
|
375
|
+
params = list(mod.params.get(id(fdef), fdef.params))
|
|
376
|
+
defaults = list((fdef.annotations or {}).get("param_defaults") or ())
|
|
377
|
+
if len(call.args) > len(params):
|
|
378
|
+
return None
|
|
379
|
+
bound = dict(enumerate(call.args))
|
|
380
|
+
for key, arg in call.kwargs.items():
|
|
381
|
+
if key not in params or params.index(key) in bound:
|
|
382
|
+
return None
|
|
383
|
+
bound[params.index(key)] = arg
|
|
384
|
+
for index in range(len(params)):
|
|
385
|
+
has_default = index < len(defaults) and defaults[index] is not None
|
|
386
|
+
if index not in bound and not has_default:
|
|
387
|
+
return None
|
|
388
|
+
return bound
|
|
389
|
+
|
|
390
|
+
def _arg_rank(self, node, site: tuple, seen: frozenset = frozenset()) -> int:
|
|
391
|
+
"""The weakest qualifier ``node`` is known to have: const for a
|
|
392
|
+
literal or a constant, simple for an input, ``syminfo.*`` /
|
|
393
|
+
``timeframe.*`` and a script's declaration of one (never reassigned,
|
|
394
|
+
not ``var``), their operators and pure calls; series otherwise. A
|
|
395
|
+
library's parameter has its declared qualifier."""
|
|
396
|
+
owner, _scope = site
|
|
397
|
+
if isinstance(node, _LITERALS):
|
|
398
|
+
return _CONST
|
|
399
|
+
if isinstance(node, Identifier):
|
|
400
|
+
if owner is not None:
|
|
401
|
+
return self._param_ranks.get(node.name, _SERIES)
|
|
402
|
+
decl = self._main_fixed_decls().get(node.name)
|
|
403
|
+
if decl is None or node.name in seen:
|
|
404
|
+
return _SERIES
|
|
405
|
+
return max(_SIMPLE, self._arg_rank(decl.value, site, seen | {node.name}))
|
|
406
|
+
if isinstance(node, MemberAccess):
|
|
407
|
+
if isinstance(node.object, Identifier):
|
|
408
|
+
if node.object.name in _SIMPLE_MEMBER_NAMESPACES:
|
|
409
|
+
return _SIMPLE
|
|
410
|
+
if node.object.name in _CONST_MEMBER_NAMESPACES:
|
|
411
|
+
return _CONST
|
|
412
|
+
return _SERIES
|
|
413
|
+
if isinstance(node, BinOp):
|
|
414
|
+
return max(self._arg_rank(node.left, site, seen),
|
|
415
|
+
self._arg_rank(node.right, site, seen))
|
|
416
|
+
if isinstance(node, UnaryOp):
|
|
417
|
+
return self._arg_rank(node.operand, site, seen)
|
|
418
|
+
if isinstance(node, Ternary):
|
|
419
|
+
return max(self._arg_rank(part, site, seen)
|
|
420
|
+
for part in (node.condition, node.true_val, node.false_val))
|
|
421
|
+
if isinstance(node, FuncCall):
|
|
422
|
+
callee = node.callee
|
|
423
|
+
parts = _callee_alias(callee)
|
|
424
|
+
if parts is not None and parts[0] == "input" or (
|
|
425
|
+
isinstance(callee, Identifier) and callee.name == "input"):
|
|
426
|
+
return _SIMPLE
|
|
427
|
+
pure = (
|
|
428
|
+
(parts is not None and (
|
|
429
|
+
parts[0] in _PURE_CALL_NAMESPACES
|
|
430
|
+
or (parts[0] == "math" and parts[1] != "random")
|
|
431
|
+
or (parts[0] == "timeframe" and parts[1] in _PURE_TIMEFRAME_CALLS)))
|
|
432
|
+
or (isinstance(callee, Identifier) and callee.name in _PURE_CALLS))
|
|
433
|
+
if not pure:
|
|
434
|
+
return _SERIES
|
|
435
|
+
return max((self._arg_rank(arg, site, seen)
|
|
436
|
+
for arg in [*node.args, *node.kwargs.values()]), default=_SIMPLE)
|
|
437
|
+
return _SERIES
|
|
438
|
+
|
|
439
|
+
def _main_fixed_decls(self) -> dict[str, object]:
|
|
440
|
+
"""The script's top-level declarations a value is read from once:
|
|
441
|
+
declared once, never reassigned, neither ``var`` nor ``series``."""
|
|
442
|
+
if self._main_fixed is None:
|
|
443
|
+
counts: dict[str, int] = {}
|
|
444
|
+
decls: dict[str, VarDecl] = {}
|
|
445
|
+
reassigned: set[str] = set()
|
|
446
|
+
for node in self._walk(self._program):
|
|
447
|
+
if isinstance(node, VarDecl):
|
|
448
|
+
counts[node.name] = counts.get(node.name, 0) + 1
|
|
449
|
+
decls[node.name] = node
|
|
450
|
+
elif isinstance(node, TupleAssign):
|
|
451
|
+
for bound in node.names:
|
|
452
|
+
counts[bound] = counts.get(bound, 0) + 2
|
|
453
|
+
elif isinstance(node, Assignment) and isinstance(node.target, Identifier):
|
|
454
|
+
reassigned.add(node.target.name)
|
|
455
|
+
top = {id(stmt) for stmt in self._program.body}
|
|
456
|
+
self._main_fixed = {
|
|
457
|
+
name: decl for name, decl in decls.items()
|
|
458
|
+
if counts.get(name) == 1 and id(decl) in top and name not in reassigned
|
|
459
|
+
and not decl.is_var and not decl.is_varip and decl.value is not None
|
|
460
|
+
and (decl.annotations or {}).get("qualifier") != "series"
|
|
461
|
+
}
|
|
462
|
+
return self._main_fixed
|
|
463
|
+
|
|
464
|
+
def _include_methods(self, mod: _Module, name: str, exported_only: bool) -> bool:
|
|
465
|
+
found = False
|
|
466
|
+
for method in mod.lib.methods.get(name, ()):
|
|
467
|
+
if exported_only and not mod.lib.exported(method):
|
|
468
|
+
continue
|
|
469
|
+
self._include(mod, method)
|
|
470
|
+
found = True
|
|
471
|
+
return found
|
|
472
|
+
|
|
473
|
+
def _include_global(self, mod: _Module, name: str) -> str:
|
|
474
|
+
self._include(mod, mod.lib.globals[name])
|
|
475
|
+
return mod.names[name]
|
|
476
|
+
|
|
477
|
+
def _include_type(self, mod: _Module, name: str) -> str:
|
|
478
|
+
node = mod.lib.types.get(name) or mod.lib.enums.get(name)
|
|
479
|
+
self._include(mod, node)
|
|
480
|
+
return mod.names[name]
|
|
481
|
+
|
|
482
|
+
def _export(self, owner: _Module | None, alias: str, member: str, at: ASTNode,
|
|
483
|
+
site: tuple | None = None):
|
|
484
|
+
"""(kind, module, new name) of ``alias.member`` read from ``owner``,
|
|
485
|
+
or None when it is the built-in namespace's member; ``site`` is a
|
|
486
|
+
call's ``(module, scope)``."""
|
|
487
|
+
builtin = BUILTIN_NAMESPACE_IMPORT_MEMBERS.get(alias)
|
|
488
|
+
if builtin is not None and member in builtin:
|
|
489
|
+
return None
|
|
490
|
+
mod = self._module_of(owner, alias, at)
|
|
491
|
+
lib = mod.lib
|
|
492
|
+
if member in lib.functions and any(lib.exported(f) for f in lib.functions[member]):
|
|
493
|
+
return "function", mod, self._include_function(mod, member, at, site)
|
|
494
|
+
if member in lib.methods and any(lib.exported(m) for m in lib.methods[member]):
|
|
495
|
+
self._include_methods(mod, member, exported_only=True)
|
|
496
|
+
return "method", mod, mod.method_names[member]
|
|
497
|
+
if member in lib.types and lib.exported(lib.types[member]):
|
|
498
|
+
return "type", mod, self._include_type(mod, member)
|
|
499
|
+
if member in lib.enums and lib.exported(lib.enums[member]):
|
|
500
|
+
return "type", mod, self._include_type(mod, member)
|
|
501
|
+
if member in lib.globals and lib.exported(lib.globals[member]):
|
|
502
|
+
return "const", mod, self._include_global(mod, member)
|
|
503
|
+
known = (member in lib.functions or member in lib.methods
|
|
504
|
+
or member in lib.types or member in lib.enums or member in lib.globals)
|
|
505
|
+
raise _LinkError(_error(
|
|
506
|
+
f"library '{mod.path}' does not export '{member}'"
|
|
507
|
+
+ ("" if known else f": it has no '{member}'"),
|
|
508
|
+
at.loc, self._filename))
|
|
509
|
+
|
|
510
|
+
# ------------------------------------------------------------------
|
|
511
|
+
# Types spelled in strings
|
|
512
|
+
# ------------------------------------------------------------------
|
|
513
|
+
|
|
514
|
+
def _rewrite_type(self, hint: str | None, owner: _Module | None,
|
|
515
|
+
at: ASTNode | None) -> str | None:
|
|
516
|
+
if not isinstance(hint, str) or not hint:
|
|
517
|
+
return hint
|
|
518
|
+
targets = self._main_targets if owner is None else owner.targets
|
|
519
|
+
|
|
520
|
+
def one(match: re.Match) -> str:
|
|
521
|
+
token = match.group(0)
|
|
522
|
+
head, _, rest = token.partition(".")
|
|
523
|
+
if rest and head in targets and "." not in rest:
|
|
524
|
+
found = self._export(owner, head, rest, at or self._program)
|
|
525
|
+
if found is None or found[0] != "type":
|
|
526
|
+
raise _LinkError(_error(
|
|
527
|
+
f"'{token}' is not a type of library '{targets[head]}'",
|
|
528
|
+
getattr(at, "loc", None), self._filename))
|
|
529
|
+
return found[2]
|
|
530
|
+
if owner is not None and not rest and (
|
|
531
|
+
token in owner.lib.types or token in owner.lib.enums):
|
|
532
|
+
return self._include_type(owner, token)
|
|
533
|
+
return token
|
|
534
|
+
|
|
535
|
+
return _TYPE_TOKEN_RE.sub(one, hint)
|
|
536
|
+
|
|
537
|
+
# ------------------------------------------------------------------
|
|
538
|
+
# Library code: renaming with scopes
|
|
539
|
+
# ------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
def _process(self, mod: _Module, node) -> None:
|
|
542
|
+
if isinstance(node, FuncDef):
|
|
543
|
+
callable_name, new_name = mod.overloads.get(
|
|
544
|
+
id(node), (node.name, mod.names[node.name]))
|
|
545
|
+
self._process_callable(mod, node, callable_name)
|
|
546
|
+
node.name = new_name
|
|
547
|
+
elif isinstance(node, MethodDef):
|
|
548
|
+
self._process_callable(mod, node, node.name)
|
|
549
|
+
node.type_name = self._rewrite_type(node.type_name, mod, node)
|
|
550
|
+
node.name = mod.method_names[node.name]
|
|
551
|
+
elif isinstance(node, TypeDecl):
|
|
552
|
+
node.name = mod.names[node.name]
|
|
553
|
+
for fld in node.fields:
|
|
554
|
+
fld.type_name = self._rewrite_type(fld.type_name, mod, node)
|
|
555
|
+
if fld.default is not None:
|
|
556
|
+
fld.default = self._expr(mod, fld.default, None)
|
|
557
|
+
elif isinstance(node, EnumDecl):
|
|
558
|
+
node.name = mod.names[node.name]
|
|
559
|
+
node.member_values = {k: self._expr(mod, v, None)
|
|
560
|
+
for k, v in node.member_values.items()}
|
|
561
|
+
elif isinstance(node, VarDecl):
|
|
562
|
+
node.type_hint = self._rewrite_type(node.type_hint, mod, node)
|
|
563
|
+
node.value = self._expr(mod, node.value, None)
|
|
564
|
+
node.name = mod.names[node.name]
|
|
565
|
+
elif isinstance(node, TupleAssign):
|
|
566
|
+
node.value = self._expr(mod, node.value, None)
|
|
567
|
+
node.names = [n if n == "_" else mod.names[n] for n in node.names]
|
|
568
|
+
|
|
569
|
+
def _process_callable(self, mod: _Module, node, callable_name: str) -> None:
|
|
570
|
+
notes = dict(node.annotations or {})
|
|
571
|
+
hints = list(notes.get("param_type_hints") or ())
|
|
572
|
+
notes["param_type_hints"] = [self._rewrite_type(h, mod, node) for h in hints]
|
|
573
|
+
defaults = list(notes.get("param_defaults") or ())
|
|
574
|
+
notes["param_defaults"] = [None if d is None else self._expr(mod, d, None)
|
|
575
|
+
for d in defaults]
|
|
576
|
+
notes["library"] = mod.path
|
|
577
|
+
notes["pine_version"] = mod.lib.pine_version
|
|
578
|
+
node.annotations = notes
|
|
579
|
+
scope = _Scope(self, mod, callable_name)
|
|
580
|
+
node.params = [scope.declare(p, hints[i] if i < len(hints) else None)
|
|
581
|
+
for i, p in enumerate(node.params)]
|
|
582
|
+
for param, qualifier in zip(node.params, notes.get("param_qualifiers") or ()):
|
|
583
|
+
self._param_ranks[param] = _QUALIFIER_RANKS.get(qualifier, _SERIES)
|
|
584
|
+
self._stmts(mod, node.body, scope)
|
|
585
|
+
|
|
586
|
+
def _stmts(self, mod: _Module, stmts: list, scope: "_Scope") -> None:
|
|
587
|
+
for index, stmt in enumerate(stmts):
|
|
588
|
+
stmts[index] = self._stmt(mod, stmt, scope)
|
|
589
|
+
|
|
590
|
+
def _stmt(self, mod: _Module, stmt, scope: "_Scope"):
|
|
591
|
+
if isinstance(stmt, VarDecl):
|
|
592
|
+
hint = stmt.type_hint
|
|
593
|
+
stmt.type_hint = self._rewrite_type(stmt.type_hint, mod, stmt)
|
|
594
|
+
stmt.value = self._expr(mod, stmt.value, scope)
|
|
595
|
+
stmt.name = scope.declare(stmt.name, hint)
|
|
596
|
+
return stmt
|
|
597
|
+
if isinstance(stmt, TupleAssign):
|
|
598
|
+
stmt.value = self._expr(mod, stmt.value, scope)
|
|
599
|
+
stmt.names = [n if n == "_" else scope.declare(n) for n in stmt.names]
|
|
600
|
+
return stmt
|
|
601
|
+
if isinstance(stmt, Assignment):
|
|
602
|
+
stmt.value = self._expr(mod, stmt.value, scope)
|
|
603
|
+
stmt.target = self._expr(mod, stmt.target, scope)
|
|
604
|
+
return stmt
|
|
605
|
+
if isinstance(stmt, ExprStmt):
|
|
606
|
+
stmt.expr = self._expr(mod, stmt.expr, scope)
|
|
607
|
+
return stmt
|
|
608
|
+
if isinstance(stmt, (BreakStmt, ContinueStmt)):
|
|
609
|
+
return stmt
|
|
610
|
+
if isinstance(stmt, _BLOCK_STATEMENTS):
|
|
611
|
+
return self._block_node(mod, stmt, scope)
|
|
612
|
+
if isinstance(stmt, ASTNode):
|
|
613
|
+
raise _LinkError(_error(
|
|
614
|
+
f"library '{mod.path}': a {type(stmt).__name__} inside a "
|
|
615
|
+
"function is not supported", stmt.loc, self._filename))
|
|
616
|
+
return stmt
|
|
617
|
+
|
|
618
|
+
def _block_node(self, mod: _Module, node, scope: "_Scope"):
|
|
619
|
+
if isinstance(node, IfStmt):
|
|
620
|
+
node.condition = self._expr(mod, node.condition, scope)
|
|
621
|
+
self._stmts(mod, node.body, scope.child())
|
|
622
|
+
self._stmts(mod, node.else_body, scope.child())
|
|
623
|
+
elif isinstance(node, ForStmt):
|
|
624
|
+
node.start = self._expr(mod, node.start, scope)
|
|
625
|
+
node.end = self._expr(mod, node.end, scope)
|
|
626
|
+
if node.step is not None:
|
|
627
|
+
node.step = self._expr(mod, node.step, scope)
|
|
628
|
+
body = scope.child()
|
|
629
|
+
node.var = body.declare(node.var) if node.var else node.var
|
|
630
|
+
self._stmts(mod, node.body, body)
|
|
631
|
+
elif isinstance(node, ForInStmt):
|
|
632
|
+
node.iterable = self._expr(mod, node.iterable, scope)
|
|
633
|
+
body = scope.child()
|
|
634
|
+
if node.var:
|
|
635
|
+
node.var = body.declare(node.var)
|
|
636
|
+
if node.vars:
|
|
637
|
+
node.vars = [v if v == "_" else body.declare(v) for v in node.vars]
|
|
638
|
+
self._stmts(mod, node.body, body)
|
|
639
|
+
elif isinstance(node, WhileStmt):
|
|
640
|
+
node.condition = self._expr(mod, node.condition, scope)
|
|
641
|
+
self._stmts(mod, node.body, scope.child())
|
|
642
|
+
elif isinstance(node, SwitchStmt):
|
|
643
|
+
if node.expr is not None:
|
|
644
|
+
node.expr = self._expr(mod, node.expr, scope)
|
|
645
|
+
cases = []
|
|
646
|
+
for case in node.cases:
|
|
647
|
+
value, body = case
|
|
648
|
+
if value is not None:
|
|
649
|
+
value = self._expr(mod, value, scope)
|
|
650
|
+
self._stmts(mod, body, scope.child())
|
|
651
|
+
cases.append((value, body))
|
|
652
|
+
node.cases = cases
|
|
653
|
+
self._stmts(mod, node.default_body, scope.child())
|
|
654
|
+
return node
|
|
655
|
+
|
|
656
|
+
def _expr(self, mod: _Module, node, scope: "_Scope | None"):
|
|
657
|
+
"""``node`` with library names resolved; a replacement node when the
|
|
658
|
+
spelling itself changes (``alias.f`` -> ``f``)."""
|
|
659
|
+
if node is None or not isinstance(node, ASTNode):
|
|
660
|
+
return node
|
|
661
|
+
if isinstance(node, Identifier):
|
|
662
|
+
local = scope.lookup(node.name) if scope is not None else None
|
|
663
|
+
if local is not None:
|
|
664
|
+
node.name = local
|
|
665
|
+
elif node.name in mod.lib.globals:
|
|
666
|
+
node.name = self._include_global(mod, node.name)
|
|
667
|
+
elif node.name in mod.lib.types or node.name in mod.lib.enums:
|
|
668
|
+
node.name = self._include_type(mod, node.name)
|
|
669
|
+
return node
|
|
670
|
+
if isinstance(node, MemberAccess):
|
|
671
|
+
parts = _callee_alias(node)
|
|
672
|
+
if parts is not None and not self._is_object(scope, parts[0]):
|
|
673
|
+
alias, member = parts
|
|
674
|
+
if alias in mod.targets:
|
|
675
|
+
found = self._export(mod, alias, member, node)
|
|
676
|
+
if found is None:
|
|
677
|
+
return node
|
|
678
|
+
kind, _target, name = found
|
|
679
|
+
if kind in ("type", "const"):
|
|
680
|
+
return self._named(name, node)
|
|
681
|
+
raise _LinkError(_error(
|
|
682
|
+
f"'{alias}.{member}' of library '{mod.targets[alias]}' is "
|
|
683
|
+
f"a {kind}, not a value", node.loc, self._filename))
|
|
684
|
+
if alias in _BUILTIN_NAMESPACES and alias not in mod.lib.globals:
|
|
685
|
+
# The built-in namespace's member, even beside a
|
|
686
|
+
# primitive local of the namespace's name.
|
|
687
|
+
self._rewrite_template_args(node, mod)
|
|
688
|
+
return node
|
|
689
|
+
node.object = self._expr(mod, node.object, scope)
|
|
690
|
+
self._rewrite_template_args(node, mod)
|
|
691
|
+
return node
|
|
692
|
+
if isinstance(node, FuncCall):
|
|
693
|
+
return self._call(mod, node, scope)
|
|
694
|
+
if isinstance(node, TypeAnnotation):
|
|
695
|
+
node.type_name = self._rewrite_type(node.type_name, mod, node)
|
|
696
|
+
return node
|
|
697
|
+
if isinstance(node, _BLOCK_STATEMENTS):
|
|
698
|
+
return self._block_node(mod, node, scope)
|
|
699
|
+
if isinstance(node, (BinOp, UnaryOp, Ternary, Subscript, TupleLiteral)):
|
|
700
|
+
for key, value in list(vars(node).items()):
|
|
701
|
+
if key in ("loc", "annotations"):
|
|
702
|
+
continue
|
|
703
|
+
if isinstance(value, ASTNode):
|
|
704
|
+
setattr(node, key, self._expr(mod, value, scope))
|
|
705
|
+
elif isinstance(value, list):
|
|
706
|
+
setattr(node, key, [self._expr(mod, v, scope) for v in value])
|
|
707
|
+
return node
|
|
708
|
+
return node
|
|
709
|
+
|
|
710
|
+
@staticmethod
|
|
711
|
+
def _is_local(scope: "_Scope | None", name: str) -> bool:
|
|
712
|
+
return scope is not None and scope.lookup(name) is not None
|
|
713
|
+
|
|
714
|
+
@staticmethod
|
|
715
|
+
def _is_object(scope: "_Scope | None", name: str) -> bool:
|
|
716
|
+
"""``name.member`` reads a local object, not a namespace or alias."""
|
|
717
|
+
if scope is None or scope.lookup(name) is None:
|
|
718
|
+
return False
|
|
719
|
+
return name not in _BUILTIN_NAMESPACES or scope.shadows_namespace(name)
|
|
720
|
+
|
|
721
|
+
@staticmethod
|
|
722
|
+
def _named(name: str, like: ASTNode) -> Identifier:
|
|
723
|
+
ident = Identifier(name=name)
|
|
724
|
+
ident.loc = like.loc
|
|
725
|
+
return ident
|
|
726
|
+
|
|
727
|
+
def _rewrite_template_args(self, node: ASTNode, owner: _Module | None) -> None:
|
|
728
|
+
notes = node.annotations or {}
|
|
729
|
+
args = notes.get("template_args")
|
|
730
|
+
if args:
|
|
731
|
+
node.annotations = dict(notes, template_args=[
|
|
732
|
+
self._rewrite_type(a, owner, node) for a in args])
|
|
733
|
+
|
|
734
|
+
def _remap_kwargs(self, mod: _Module, node: FuncCall, callable_name: str,
|
|
735
|
+
params: set[str]) -> None:
|
|
736
|
+
if node.kwargs:
|
|
737
|
+
node.kwargs = {
|
|
738
|
+
(self._local_name(mod, callable_name, k) if k in params else k): v
|
|
739
|
+
for k, v in node.kwargs.items()
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
def _function_params(self, mod: _Module, name: str) -> set[str]:
|
|
743
|
+
return {p for f in mod.lib.functions.get(name, ())
|
|
744
|
+
for p in mod.params.get(id(f), f.params)}
|
|
745
|
+
|
|
746
|
+
def _method_params(self, mod: _Module, name: str) -> set[str]:
|
|
747
|
+
return {p for m in mod.lib.methods.get(name, ())
|
|
748
|
+
for p in mod.params.get(id(m), m.params)[1:]}
|
|
749
|
+
|
|
750
|
+
def _call_args(self, owner: _Module | None, node: FuncCall, scope) -> None:
|
|
751
|
+
"""Resolve ``node``'s arguments; keep its written argument order."""
|
|
752
|
+
replaced: dict[int, ASTNode] = {}
|
|
753
|
+
args = []
|
|
754
|
+
for arg in node.args:
|
|
755
|
+
new = self._any_expr(owner, arg, scope)
|
|
756
|
+
if new is not arg:
|
|
757
|
+
replaced[id(arg)] = new
|
|
758
|
+
args.append(new)
|
|
759
|
+
kwargs = {}
|
|
760
|
+
for key, arg in node.kwargs.items():
|
|
761
|
+
new = self._any_expr(owner, arg, scope)
|
|
762
|
+
if new is not arg:
|
|
763
|
+
replaced[id(arg)] = new
|
|
764
|
+
kwargs[key] = new
|
|
765
|
+
node.args, node.kwargs = args, kwargs
|
|
766
|
+
notes = node.annotations or {}
|
|
767
|
+
order = notes.get("call_arg_order")
|
|
768
|
+
if replaced and isinstance(order, ArgOrder):
|
|
769
|
+
node.annotations = dict(notes, call_arg_order=ArgOrder(
|
|
770
|
+
replaced.get(id(n), n) for n in order))
|
|
771
|
+
|
|
772
|
+
def _any_expr(self, owner: _Module | None, node, scope):
|
|
773
|
+
if owner is None:
|
|
774
|
+
return self._main_expr(node)
|
|
775
|
+
return self._expr(owner, node, scope)
|
|
776
|
+
|
|
777
|
+
def _as_method_call(self, node: FuncCall, member: str, at: ASTNode) -> FuncCall:
|
|
778
|
+
"""``alias.m(recv, ...)`` -> ``recv.m(...)``."""
|
|
779
|
+
if not node.args:
|
|
780
|
+
raise _LinkError(_error(
|
|
781
|
+
f"library method '{member}' called without its receiver as the "
|
|
782
|
+
"first positional argument", at.loc, self._filename))
|
|
783
|
+
receiver, rest = node.args[0], node.args[1:]
|
|
784
|
+
callee = MemberAccess(object=receiver, member=member)
|
|
785
|
+
callee.loc = node.callee.loc
|
|
786
|
+
node.callee = callee
|
|
787
|
+
node.args = rest
|
|
788
|
+
notes = node.annotations or {}
|
|
789
|
+
order = notes.get("call_arg_order")
|
|
790
|
+
if isinstance(order, ArgOrder):
|
|
791
|
+
node.annotations = dict(notes, call_arg_order=ArgOrder(
|
|
792
|
+
n for n in order if n is not receiver))
|
|
793
|
+
return node
|
|
794
|
+
|
|
795
|
+
def _call(self, mod: _Module, node: FuncCall, scope) -> FuncCall:
|
|
796
|
+
self._call_args(mod, node, scope)
|
|
797
|
+
callee = node.callee
|
|
798
|
+
if isinstance(callee, Identifier) and not self._is_local(scope, callee.name):
|
|
799
|
+
name = callee.name
|
|
800
|
+
if name in mod.lib.functions:
|
|
801
|
+
new_name = self._include_function(mod, name, node, (mod, scope))
|
|
802
|
+
self._remap_kwargs(mod, node, *self._callable_of(mod, name, node))
|
|
803
|
+
callee.name = new_name
|
|
804
|
+
return node
|
|
805
|
+
if name in mod.lib.methods:
|
|
806
|
+
# A method called as a function: ``m(recv, ...)``.
|
|
807
|
+
self._include_methods(mod, name, exported_only=False)
|
|
808
|
+
self._remap_kwargs(mod, node, name, self._method_params(mod, name))
|
|
809
|
+
return self._as_method_call(node, mod.method_names[name], node)
|
|
810
|
+
return node
|
|
811
|
+
parts = _callee_alias(callee)
|
|
812
|
+
if parts is not None and not self._is_object(scope, parts[0]):
|
|
813
|
+
alias, member = parts
|
|
814
|
+
if alias in mod.targets:
|
|
815
|
+
found = self._export(mod, alias, member, node, (mod, scope))
|
|
816
|
+
if found is None:
|
|
817
|
+
return node
|
|
818
|
+
kind, target, name = found
|
|
819
|
+
if kind == "function":
|
|
820
|
+
self._remap_kwargs(target, node, *self._callable_of(target, member, node))
|
|
821
|
+
node.callee = self._named(name, callee)
|
|
822
|
+
return node
|
|
823
|
+
if kind == "method":
|
|
824
|
+
self._remap_kwargs(target, node, member, self._method_params(target, member))
|
|
825
|
+
return self._as_method_call(node, name, node)
|
|
826
|
+
raise _LinkError(_error(
|
|
827
|
+
f"'{alias}.{member}' of library '{mod.targets[alias]}' is a "
|
|
828
|
+
f"{kind}, not a function", node.loc, self._filename))
|
|
829
|
+
if alias in mod.lib.types or alias in mod.lib.enums:
|
|
830
|
+
callee.object = self._named(self._include_type(mod, alias), callee.object)
|
|
831
|
+
self._rewrite_template_args(callee, mod)
|
|
832
|
+
return node
|
|
833
|
+
if alias in _BUILTIN_NAMESPACES:
|
|
834
|
+
self._rewrite_template_args(callee, mod)
|
|
835
|
+
return node
|
|
836
|
+
if isinstance(callee, MemberAccess):
|
|
837
|
+
callee.object = self._expr(mod, callee.object, scope)
|
|
838
|
+
self._rewrite_template_args(callee, mod)
|
|
839
|
+
self._method_call(mod, node, callee.member)
|
|
840
|
+
return node
|
|
841
|
+
|
|
842
|
+
def _method_call(self, owner: _Module | None, node: FuncCall, member: str) -> None:
|
|
843
|
+
"""``recv.member(...)``: the library methods it may bind to (methods
|
|
844
|
+
bind by receiver type, so every candidate is inlined)."""
|
|
845
|
+
if owner is not None and member in owner.lib.methods:
|
|
846
|
+
self._include_methods(owner, member, exported_only=False)
|
|
847
|
+
self._remap_kwargs(owner, node, member, self._method_params(owner, member))
|
|
848
|
+
node.callee.member = owner.method_names[member]
|
|
849
|
+
return
|
|
850
|
+
if owner is None:
|
|
851
|
+
# The script's unloaded imports are unpinned and unnamed: linked
|
|
852
|
+
# by nothing.
|
|
853
|
+
candidates = [self._modules[p] for p in self._main_targets.values()
|
|
854
|
+
if p in self._modules]
|
|
855
|
+
else:
|
|
856
|
+
# TradingView links every import of a linked library.
|
|
857
|
+
candidates = [self._load(path, node) for path in owner.targets.values()]
|
|
858
|
+
exporters = []
|
|
859
|
+
for mod in candidates:
|
|
860
|
+
if self._include_methods(mod, member, exported_only=True):
|
|
861
|
+
exporters.append(mod)
|
|
862
|
+
deps = self._main_deps if owner is None else owner.deps
|
|
863
|
+
if mod.path not in deps:
|
|
864
|
+
deps.append(mod.path)
|
|
865
|
+
if exporters and node.kwargs:
|
|
866
|
+
if len(exporters) > 1:
|
|
867
|
+
raise _LinkError(_error(
|
|
868
|
+
f"method '{member}' is exported by {len(exporters)} imported "
|
|
869
|
+
"libraries; a keyword argument cannot be bound to one",
|
|
870
|
+
node.loc, self._filename))
|
|
871
|
+
target = exporters[0]
|
|
872
|
+
self._remap_kwargs(target, node, member, self._method_params(target, member))
|
|
873
|
+
|
|
874
|
+
# ------------------------------------------------------------------
|
|
875
|
+
# The importing script
|
|
876
|
+
# ------------------------------------------------------------------
|
|
877
|
+
|
|
878
|
+
def _main_expr(self, node):
|
|
879
|
+
if node is None or not isinstance(node, ASTNode):
|
|
880
|
+
return node
|
|
881
|
+
if isinstance(node, MemberAccess):
|
|
882
|
+
parts = _callee_alias(node)
|
|
883
|
+
if parts is not None and parts[0] in self._main_targets:
|
|
884
|
+
alias, member = parts
|
|
885
|
+
found = self._export(None, alias, member, node)
|
|
886
|
+
if found is None:
|
|
887
|
+
return node
|
|
888
|
+
kind, _mod, name = found
|
|
889
|
+
if kind in ("type", "const"):
|
|
890
|
+
return self._named(name, node)
|
|
891
|
+
raise _LinkError(_error(
|
|
892
|
+
f"'{alias}.{member}' of library '{self._main_targets[alias]}' "
|
|
893
|
+
f"is a {kind}, not a value", node.loc, self._filename))
|
|
894
|
+
node.object = self._main_expr(node.object)
|
|
895
|
+
self._rewrite_template_args(node, None)
|
|
896
|
+
return node
|
|
897
|
+
if isinstance(node, FuncCall):
|
|
898
|
+
self._call_args(None, node, None)
|
|
899
|
+
callee = node.callee
|
|
900
|
+
parts = _callee_alias(callee)
|
|
901
|
+
if parts is not None and parts[0] in self._main_targets:
|
|
902
|
+
alias, member = parts
|
|
903
|
+
found = self._export(None, alias, member, node, (None, None))
|
|
904
|
+
if found is None:
|
|
905
|
+
self._rewrite_template_args(callee, None)
|
|
906
|
+
return node
|
|
907
|
+
kind, target, name = found
|
|
908
|
+
if kind == "function":
|
|
909
|
+
self._remap_kwargs(target, node, *self._callable_of(target, member, node))
|
|
910
|
+
node.callee = self._named(name, callee)
|
|
911
|
+
return node
|
|
912
|
+
if kind == "method":
|
|
913
|
+
self._remap_kwargs(target, node, member, self._method_params(target, member))
|
|
914
|
+
return self._as_method_call(node, name, node)
|
|
915
|
+
raise _LinkError(_error(
|
|
916
|
+
f"'{alias}.{member}' of library '{self._main_targets[alias]}' is "
|
|
917
|
+
f"a {kind}, not a function", node.loc, self._filename))
|
|
918
|
+
if isinstance(callee, MemberAccess):
|
|
919
|
+
callee.object = self._main_expr(callee.object)
|
|
920
|
+
self._rewrite_template_args(callee, None)
|
|
921
|
+
if not (isinstance(callee.object, Identifier)
|
|
922
|
+
and callee.object.name in _BUILTIN_NAMESPACES):
|
|
923
|
+
self._method_call(None, node, callee.member)
|
|
924
|
+
else:
|
|
925
|
+
node.callee = self._main_expr(callee)
|
|
926
|
+
return node
|
|
927
|
+
if isinstance(node, TypeAnnotation):
|
|
928
|
+
node.type_name = self._rewrite_type(node.type_name, None, node)
|
|
929
|
+
return node
|
|
930
|
+
self._main_children(node)
|
|
931
|
+
return node
|
|
932
|
+
|
|
933
|
+
def _main_children(self, node: ASTNode) -> None:
|
|
934
|
+
if isinstance(node, (FuncDef, MethodDef)):
|
|
935
|
+
notes = dict(node.annotations or {})
|
|
936
|
+
if "param_type_hints" in notes:
|
|
937
|
+
notes["param_type_hints"] = [
|
|
938
|
+
self._rewrite_type(h, None, node) for h in notes["param_type_hints"]]
|
|
939
|
+
if "param_defaults" in notes:
|
|
940
|
+
notes["param_defaults"] = [
|
|
941
|
+
None if d is None else self._main_expr(d)
|
|
942
|
+
for d in notes["param_defaults"]]
|
|
943
|
+
node.annotations = notes
|
|
944
|
+
if isinstance(node, MethodDef):
|
|
945
|
+
node.type_name = self._rewrite_type(node.type_name, None, node)
|
|
946
|
+
if isinstance(node, VarDecl):
|
|
947
|
+
node.type_hint = self._rewrite_type(node.type_hint, None, node)
|
|
948
|
+
if isinstance(node, TypeDecl):
|
|
949
|
+
for fld in node.fields:
|
|
950
|
+
fld.type_name = self._rewrite_type(fld.type_name, None, node)
|
|
951
|
+
if fld.default is not None:
|
|
952
|
+
fld.default = self._main_expr(fld.default)
|
|
953
|
+
return
|
|
954
|
+
if isinstance(node, SwitchStmt):
|
|
955
|
+
if node.expr is not None:
|
|
956
|
+
node.expr = self._main_expr(node.expr)
|
|
957
|
+
node.cases = [
|
|
958
|
+
(None if value is None else self._main_expr(value),
|
|
959
|
+
[self._main_expr(s) for s in body])
|
|
960
|
+
for value, body in node.cases]
|
|
961
|
+
node.default_body = [self._main_expr(s) for s in node.default_body]
|
|
962
|
+
return
|
|
963
|
+
for key, value in list(vars(node).items()):
|
|
964
|
+
if key in ("loc", "annotations"):
|
|
965
|
+
continue
|
|
966
|
+
if isinstance(value, ASTNode):
|
|
967
|
+
setattr(node, key, self._main_expr(value))
|
|
968
|
+
elif isinstance(value, list):
|
|
969
|
+
setattr(node, key, [self._main_expr(v) if isinstance(v, ASTNode) else v
|
|
970
|
+
for v in value])
|
|
971
|
+
elif isinstance(value, dict):
|
|
972
|
+
setattr(node, key, {k: self._main_expr(v) if isinstance(v, ASTNode) else v
|
|
973
|
+
for k, v in value.items()})
|
|
974
|
+
|
|
975
|
+
# ------------------------------------------------------------------
|
|
976
|
+
# Entry
|
|
977
|
+
# ------------------------------------------------------------------
|
|
978
|
+
|
|
979
|
+
def link(self, handled: list[ImportStmt]) -> Program:
|
|
980
|
+
self._collect_names(self._program)
|
|
981
|
+
declared = self._declared_main_names()
|
|
982
|
+
for stmt in handled:
|
|
983
|
+
alias = stmt.alias or stmt.name
|
|
984
|
+
if alias in self._main_targets:
|
|
985
|
+
raise _LinkError(_error(
|
|
986
|
+
f"import alias '{alias}' is used by two imports", stmt.loc,
|
|
987
|
+
self._filename))
|
|
988
|
+
if alias in _BUILTIN_NAMESPACES and alias not in BUILTIN_NAMESPACE_IMPORT_MEMBERS:
|
|
989
|
+
raise _LinkError(_error(
|
|
990
|
+
f"Import is not supported: '{import_spelling(stmt)}': its alias "
|
|
991
|
+
f"'{alias}' is a built-in namespace other than ta, math or str",
|
|
992
|
+
stmt.loc, self._filename))
|
|
993
|
+
if alias in declared:
|
|
994
|
+
raise _LinkError(_error(
|
|
995
|
+
f"import alias '{alias}' is also declared as a variable, "
|
|
996
|
+
"parameter or function", stmt.loc, self._filename))
|
|
997
|
+
self._main_targets[alias] = stmt.path
|
|
998
|
+
# A library whose alias the script never names is still linked when
|
|
999
|
+
# its source is at hand (its exported methods bind by receiver type);
|
|
1000
|
+
# otherwise TradingView links nothing for it and it is dropped.
|
|
1001
|
+
referenced = self._referenced_aliases()
|
|
1002
|
+
for stmt in handled:
|
|
1003
|
+
alias = stmt.alias or stmt.name
|
|
1004
|
+
if alias in referenced or self._resolver.pinned(stmt.path):
|
|
1005
|
+
self._module_of(None, alias, stmt)
|
|
1006
|
+
body = [self._main_expr(s) if not isinstance(s, ImportStmt) else s
|
|
1007
|
+
for s in self._program.body]
|
|
1008
|
+
while self._queue:
|
|
1009
|
+
mod, node = self._queue.pop(0)
|
|
1010
|
+
self._process(mod, node)
|
|
1011
|
+
self._finish()
|
|
1012
|
+
handled_ids = {id(s) for s in handled}
|
|
1013
|
+
definitions = self._definitions()
|
|
1014
|
+
self._check_v5_in_requests([*definitions, *body])
|
|
1015
|
+
merged: list = []
|
|
1016
|
+
inserted = False
|
|
1017
|
+
first_use = self._first_library_use(body, handled_ids)
|
|
1018
|
+
for index, stmt in enumerate(body):
|
|
1019
|
+
if not inserted and (id(stmt) in handled_ids or index == first_use):
|
|
1020
|
+
merged.extend(definitions)
|
|
1021
|
+
inserted = True
|
|
1022
|
+
if id(stmt) in handled_ids:
|
|
1023
|
+
continue
|
|
1024
|
+
merged.append(stmt)
|
|
1025
|
+
if not inserted:
|
|
1026
|
+
merged.extend(definitions)
|
|
1027
|
+
program = Program(body=merged, version=self._program.version)
|
|
1028
|
+
program.loc = self._program.loc
|
|
1029
|
+
program.annotations = self._program.annotations
|
|
1030
|
+
return program
|
|
1031
|
+
|
|
1032
|
+
def _finish(self) -> None:
|
|
1033
|
+
"""Checks that need every reachable definition."""
|
|
1034
|
+
# Methods bind by receiver type and name: two with one key would
|
|
1035
|
+
# leave one of them silently unreachable.
|
|
1036
|
+
owners: dict[str, str] = {}
|
|
1037
|
+
for stmt in self._program.body:
|
|
1038
|
+
if isinstance(stmt, MethodDef):
|
|
1039
|
+
owners.setdefault(f"{stmt.type_name}.{stmt.name}", "the script")
|
|
1040
|
+
for path in self._load_order:
|
|
1041
|
+
for stmt in self._ordered_included(self._modules[path]):
|
|
1042
|
+
if not isinstance(stmt, MethodDef):
|
|
1043
|
+
continue
|
|
1044
|
+
key = f"{stmt.type_name}.{stmt.name}"
|
|
1045
|
+
other = owners.setdefault(key, f"library '{path}'")
|
|
1046
|
+
if other != f"library '{path}'":
|
|
1047
|
+
raise _LinkError(_error(
|
|
1048
|
+
f"method '{stmt.name}' of library '{path}' has the same "
|
|
1049
|
+
f"receiver type as a method of {other}", stmt.loc,
|
|
1050
|
+
self._filename))
|
|
1051
|
+
from .library_v5 import lower_v5_modules
|
|
1052
|
+
lower_v5_modules(
|
|
1053
|
+
[(self._modules[p].lib, self._ordered_included(self._modules[p]),
|
|
1054
|
+
self._modules[p].names)
|
|
1055
|
+
for p in self._load_order if self._modules[p].included],
|
|
1056
|
+
self._filename,
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
def _check_v5_in_requests(self, statements: list) -> None:
|
|
1060
|
+
"""A request.*() payload is evaluated by the request's own builder,
|
|
1061
|
+
which inlines the user functions it calls outside the codegen's v5
|
|
1062
|
+
rules: refuse a v5 library function a payload reaches."""
|
|
1063
|
+
v5: dict[str, str] = {}
|
|
1064
|
+
methods: dict[str, str] = {}
|
|
1065
|
+
for path in self._load_order:
|
|
1066
|
+
mod = self._modules[path]
|
|
1067
|
+
if mod.lib.pine_version != 5:
|
|
1068
|
+
continue
|
|
1069
|
+
for stmt in self._ordered_included(mod):
|
|
1070
|
+
if isinstance(stmt, FuncDef):
|
|
1071
|
+
v5[stmt.name] = path
|
|
1072
|
+
elif isinstance(stmt, MethodDef):
|
|
1073
|
+
methods[stmt.name] = path
|
|
1074
|
+
if not v5 and not methods:
|
|
1075
|
+
return
|
|
1076
|
+
funcs = {s.name: s for s in statements if isinstance(s, (FuncDef, MethodDef))}
|
|
1077
|
+
for node in self._walk(statements):
|
|
1078
|
+
if not (isinstance(node, FuncCall)
|
|
1079
|
+
and _callee_alias(node.callee) is not None
|
|
1080
|
+
and node.callee.object.name == "request"):
|
|
1081
|
+
continue
|
|
1082
|
+
seen: set[str] = set()
|
|
1083
|
+
stack = [*node.args, *node.kwargs.values()]
|
|
1084
|
+
while stack:
|
|
1085
|
+
for sub in self._walk(stack.pop()):
|
|
1086
|
+
if not isinstance(sub, FuncCall):
|
|
1087
|
+
continue
|
|
1088
|
+
callee = sub.callee
|
|
1089
|
+
name = callee.name if isinstance(callee, Identifier) else (
|
|
1090
|
+
callee.member if isinstance(callee, MemberAccess) else None)
|
|
1091
|
+
path = v5.get(name) if isinstance(callee, Identifier) else methods.get(name)
|
|
1092
|
+
if path is not None:
|
|
1093
|
+
raise _LinkError(_error(
|
|
1094
|
+
f"library '{path}' is //@version=5: its function "
|
|
1095
|
+
f"reached from a request.{node.callee.member}() "
|
|
1096
|
+
"payload would run outside v5's rules there",
|
|
1097
|
+
sub.loc, self._filename))
|
|
1098
|
+
if name in funcs and name not in seen:
|
|
1099
|
+
seen.add(name)
|
|
1100
|
+
stack.append(funcs[name].body)
|
|
1101
|
+
|
|
1102
|
+
def _ordered_included(self, mod: _Module) -> list:
|
|
1103
|
+
return [stmt for stmt in mod.lib.program.body if id(stmt) in mod.included]
|
|
1104
|
+
|
|
1105
|
+
def _definitions(self) -> list:
|
|
1106
|
+
"""Every reachable library definition, a module after the modules its
|
|
1107
|
+
code references, each module's in its source order."""
|
|
1108
|
+
order: list[str] = []
|
|
1109
|
+
visiting: set[str] = set()
|
|
1110
|
+
|
|
1111
|
+
def visit(path: str) -> None:
|
|
1112
|
+
if path in order or path in visiting:
|
|
1113
|
+
return
|
|
1114
|
+
visiting.add(path)
|
|
1115
|
+
for dep in self._modules[path].deps:
|
|
1116
|
+
visit(dep)
|
|
1117
|
+
visiting.discard(path)
|
|
1118
|
+
order.append(path)
|
|
1119
|
+
|
|
1120
|
+
for path in self._main_deps:
|
|
1121
|
+
visit(path)
|
|
1122
|
+
for path in self._load_order:
|
|
1123
|
+
visit(path)
|
|
1124
|
+
out: list = []
|
|
1125
|
+
for path in order:
|
|
1126
|
+
out.extend(self._ordered_included(self._modules[path]))
|
|
1127
|
+
return out
|
|
1128
|
+
|
|
1129
|
+
def _first_library_use(self, body: list, handled_ids: set[int]) -> int | None:
|
|
1130
|
+
"""Index of the first statement before every handled import that
|
|
1131
|
+
reads a library name (None: the first import comes first)."""
|
|
1132
|
+
names = {n for m in self._modules.values() for n in m.names.values()}
|
|
1133
|
+
names.update(n for m in self._modules.values() for n in m.method_names.values())
|
|
1134
|
+
for index, stmt in enumerate(body):
|
|
1135
|
+
if id(stmt) in handled_ids:
|
|
1136
|
+
return None
|
|
1137
|
+
if isinstance(stmt, StrategyDecl):
|
|
1138
|
+
continue
|
|
1139
|
+
found: list[str] = []
|
|
1140
|
+
self._scan_identifiers(stmt, found)
|
|
1141
|
+
if names.intersection(found):
|
|
1142
|
+
return index
|
|
1143
|
+
return None
|
|
1144
|
+
|
|
1145
|
+
@staticmethod
|
|
1146
|
+
def _scan_identifiers(root, out: list) -> None:
|
|
1147
|
+
stack = [root]
|
|
1148
|
+
while stack:
|
|
1149
|
+
node = stack.pop()
|
|
1150
|
+
if isinstance(node, (list, tuple)):
|
|
1151
|
+
stack.extend(node)
|
|
1152
|
+
elif isinstance(node, dict):
|
|
1153
|
+
stack.extend(node.values())
|
|
1154
|
+
elif isinstance(node, ASTNode):
|
|
1155
|
+
if isinstance(node, Identifier):
|
|
1156
|
+
out.append(node.name)
|
|
1157
|
+
elif isinstance(node, MemberAccess):
|
|
1158
|
+
out.append(node.member)
|
|
1159
|
+
for key, value in vars(node).items():
|
|
1160
|
+
if key not in ("loc", "annotations"):
|
|
1161
|
+
stack.append(value)
|
|
1162
|
+
|
|
1163
|
+
def _declared_main_names(self) -> set[str]:
|
|
1164
|
+
names: set[str] = set()
|
|
1165
|
+
for node in self._walk(self._program):
|
|
1166
|
+
if isinstance(node, (VarDecl, FuncDef, TypeDecl, EnumDecl)):
|
|
1167
|
+
names.add(node.name)
|
|
1168
|
+
if isinstance(node, (FuncDef, MethodDef)):
|
|
1169
|
+
names.update(node.params)
|
|
1170
|
+
if isinstance(node, TupleAssign):
|
|
1171
|
+
names.update(node.names)
|
|
1172
|
+
if isinstance(node, ForStmt) and node.var:
|
|
1173
|
+
names.add(node.var)
|
|
1174
|
+
if isinstance(node, ForInStmt):
|
|
1175
|
+
if node.var:
|
|
1176
|
+
names.add(node.var)
|
|
1177
|
+
names.update(node.vars or ())
|
|
1178
|
+
return names
|
|
1179
|
+
|
|
1180
|
+
def _referenced_aliases(self) -> set[str]:
|
|
1181
|
+
"""Main aliases the script names with a library member."""
|
|
1182
|
+
found: set[str] = set()
|
|
1183
|
+
for node in self._walk(self._program):
|
|
1184
|
+
parts = _callee_alias(node)
|
|
1185
|
+
if parts is not None and parts[0] in self._main_targets:
|
|
1186
|
+
alias, member = parts
|
|
1187
|
+
builtin = BUILTIN_NAMESPACE_IMPORT_MEMBERS.get(alias)
|
|
1188
|
+
if builtin is None or member not in builtin:
|
|
1189
|
+
found.add(alias)
|
|
1190
|
+
for hint in self._type_strings(node):
|
|
1191
|
+
for token in _TYPE_TOKEN_RE.findall(hint):
|
|
1192
|
+
head, _, rest = token.partition(".")
|
|
1193
|
+
if rest and head in self._main_targets:
|
|
1194
|
+
found.add(head)
|
|
1195
|
+
return found
|
|
1196
|
+
|
|
1197
|
+
@staticmethod
|
|
1198
|
+
def _type_strings(node) -> list[str]:
|
|
1199
|
+
out = []
|
|
1200
|
+
for key in ("type_hint", "type_name"):
|
|
1201
|
+
value = getattr(node, key, None)
|
|
1202
|
+
if isinstance(value, str):
|
|
1203
|
+
out.append(value)
|
|
1204
|
+
notes = getattr(node, "annotations", None) or {}
|
|
1205
|
+
for key in ("param_type_hints", "template_args"):
|
|
1206
|
+
out.extend(h for h in notes.get(key) or () if isinstance(h, str))
|
|
1207
|
+
if isinstance(node, TypeDecl):
|
|
1208
|
+
out.extend(f.type_name for f in node.fields if isinstance(f.type_name, str))
|
|
1209
|
+
return out
|
|
1210
|
+
|
|
1211
|
+
@staticmethod
|
|
1212
|
+
def _walk(root):
|
|
1213
|
+
stack = [root]
|
|
1214
|
+
while stack:
|
|
1215
|
+
node = stack.pop()
|
|
1216
|
+
if isinstance(node, (list, tuple)):
|
|
1217
|
+
stack.extend(node)
|
|
1218
|
+
continue
|
|
1219
|
+
if isinstance(node, dict):
|
|
1220
|
+
stack.extend(node.values())
|
|
1221
|
+
continue
|
|
1222
|
+
if isinstance(node, TypeField):
|
|
1223
|
+
stack.append(node.default)
|
|
1224
|
+
continue
|
|
1225
|
+
if not isinstance(node, ASTNode):
|
|
1226
|
+
continue
|
|
1227
|
+
yield node
|
|
1228
|
+
for key, value in vars(node).items():
|
|
1229
|
+
if key == "loc":
|
|
1230
|
+
continue
|
|
1231
|
+
if key == "annotations":
|
|
1232
|
+
stack.extend(d for d in (value or {}).get("param_defaults") or ()
|
|
1233
|
+
if d is not None)
|
|
1234
|
+
continue
|
|
1235
|
+
stack.append(value)
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
class _Scope:
|
|
1239
|
+
"""The locals and parameters visible at one point of an inlined body."""
|
|
1240
|
+
|
|
1241
|
+
def __init__(self, linker: _Linker, mod: _Module, callable_name: str,
|
|
1242
|
+
parent: "_Scope | None" = None) -> None:
|
|
1243
|
+
self._linker = linker
|
|
1244
|
+
self._mod = mod
|
|
1245
|
+
self._callable = callable_name
|
|
1246
|
+
self._parent = parent
|
|
1247
|
+
self._names: dict[str, str] = {}
|
|
1248
|
+
self._hints: dict[str, str | None] = {}
|
|
1249
|
+
|
|
1250
|
+
def child(self) -> "_Scope":
|
|
1251
|
+
return _Scope(self._linker, self._mod, self._callable, self)
|
|
1252
|
+
|
|
1253
|
+
def declare(self, name: str, hint: str | None = None) -> str:
|
|
1254
|
+
new = self._linker._local_name(self._mod, self._callable, name)
|
|
1255
|
+
self._names[name] = new
|
|
1256
|
+
self._hints[name] = hint
|
|
1257
|
+
return new
|
|
1258
|
+
|
|
1259
|
+
def lookup(self, name: str) -> str | None:
|
|
1260
|
+
scope: _Scope | None = self
|
|
1261
|
+
while scope is not None:
|
|
1262
|
+
if name in scope._names:
|
|
1263
|
+
return scope._names[name]
|
|
1264
|
+
scope = scope._parent
|
|
1265
|
+
return None
|
|
1266
|
+
|
|
1267
|
+
def shadows_namespace(self, name: str) -> bool:
|
|
1268
|
+
"""``name`` is a local that ``name.member`` reads as an object: not
|
|
1269
|
+
one of a primitive type, which has no members, so that a parameter
|
|
1270
|
+
named ``timeframe`` (TradingView/RelativeValue/2) leaves
|
|
1271
|
+
``timeframe.change(...)`` the built-in."""
|
|
1272
|
+
scope: _Scope | None = self
|
|
1273
|
+
while scope is not None:
|
|
1274
|
+
if name in scope._names:
|
|
1275
|
+
hint = scope._hints.get(name)
|
|
1276
|
+
return bool(hint) and hint not in ("int", "float", "bool", "string", "color")
|
|
1277
|
+
scope = scope._parent
|
|
1278
|
+
return False
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def inline_libraries(program: Program, source: str, *,
|
|
1282
|
+
libraries: Mapping[str, str] | None,
|
|
1283
|
+
filename: str = "<input>",
|
|
1284
|
+
budget: TimeBudget | None = None) -> Program:
|
|
1285
|
+
"""``program`` with the libraries it imports inlined (see module doc)."""
|
|
1286
|
+
handled = [
|
|
1287
|
+
stmt for stmt in program.body
|
|
1288
|
+
if isinstance(stmt, ImportStmt) and stmt.version is not None
|
|
1289
|
+
and not import_is_builtin_namespace_no_op(program, stmt)
|
|
1290
|
+
]
|
|
1291
|
+
if not handled:
|
|
1292
|
+
return program
|
|
1293
|
+
resolver = library_resolver(source, libraries)
|
|
1294
|
+
if not resolver.configured:
|
|
1295
|
+
if resolver.reason:
|
|
1296
|
+
for stmt in handled:
|
|
1297
|
+
stmt.annotations = {**(stmt.annotations or {}),
|
|
1298
|
+
"unresolved_reason": resolver.reason}
|
|
1299
|
+
return program
|
|
1300
|
+
linker = _Linker(program, resolver, filename, budget)
|
|
1301
|
+
try:
|
|
1302
|
+
return linker.link(handled)
|
|
1303
|
+
except _LinkError as exc:
|
|
1304
|
+
raise CompileError([exc.diagnostic]) from None
|