@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.
Files changed (50) hide show
  1. package/README.md +16 -16
  2. package/glue.py +24 -16
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +125 -34
  5. package/pineforge_codegen/analyzer/__init__.py +2 -0
  6. package/pineforge_codegen/analyzer/base.py +754 -76
  7. package/pineforge_codegen/analyzer/call_handlers.py +260 -40
  8. package/pineforge_codegen/analyzer/contracts.py +37 -0
  9. package/pineforge_codegen/analyzer/diagnostics.py +30 -4
  10. package/pineforge_codegen/analyzer/tables.py +49 -8
  11. package/pineforge_codegen/analyzer/types.py +33 -1
  12. package/pineforge_codegen/ast_nodes.py +32 -1
  13. package/pineforge_codegen/block_locals.py +185 -0
  14. package/pineforge_codegen/builtin_keywords.py +42 -0
  15. package/pineforge_codegen/codegen/base.py +896 -156
  16. package/pineforge_codegen/codegen/constant_fold.py +131 -0
  17. package/pineforge_codegen/codegen/drawing.py +221 -79
  18. package/pineforge_codegen/codegen/emit_top.py +946 -213
  19. package/pineforge_codegen/codegen/helpers.py +435 -14
  20. package/pineforge_codegen/codegen/host_members.py +162 -0
  21. package/pineforge_codegen/codegen/input.py +252 -85
  22. package/pineforge_codegen/codegen/security.py +4372 -377
  23. package/pineforge_codegen/codegen/session_market.py +71 -0
  24. package/pineforge_codegen/codegen/ta.py +1188 -100
  25. package/pineforge_codegen/codegen/tables.py +193 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +1882 -78
  28. package/pineforge_codegen/codegen/visit_call.py +920 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +738 -57
  30. package/pineforge_codegen/codegen/visit_stmt.py +595 -49
  31. package/pineforge_codegen/external_requests.py +877 -0
  32. package/pineforge_codegen/lexer.py +104 -22
  33. package/pineforge_codegen/library_inline.py +1304 -0
  34. package/pineforge_codegen/library_modules.py +126 -0
  35. package/pineforge_codegen/library_v5.py +683 -0
  36. package/pineforge_codegen/limits.py +138 -0
  37. package/pineforge_codegen/method_binding.py +33 -0
  38. package/pineforge_codegen/parser.py +384 -68
  39. package/pineforge_codegen/pine_libraries.py +266 -0
  40. package/pineforge_codegen/pine_spelling.py +216 -0
  41. package/pineforge_codegen/pragmas.py +64 -10
  42. package/pineforge_codegen/security_contexts.py +1585 -0
  43. package/pineforge_codegen/session_reads.py +84 -0
  44. package/pineforge_codegen/signatures.py +48 -23
  45. package/pineforge_codegen/support_checker.py +1106 -85
  46. package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
  47. package/release.json +2 -2
  48. package/tables.json +23 -21
  49. package/transpile.worker.mjs +24 -16
  50. 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