@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,1585 @@
|
|
|
1
|
+
"""One ``request.security`` context per symbol and timeframe reaching a helper.
|
|
2
|
+
|
|
3
|
+
A request's context -- the symbol and the timeframe it aggregates -- is fixed
|
|
4
|
+
before the first bar: ``register_security_eval`` runs before ``on_bar``. When
|
|
5
|
+
a helper receives them as parameters, the context is the value of those
|
|
6
|
+
parameters on each call path, and a path may pass a parameter on through
|
|
7
|
+
further helpers::
|
|
8
|
+
|
|
9
|
+
g(sym, tf) => request.security(sym, tf, ta.sma(close, 3))
|
|
10
|
+
h(sym, tf) => g(sym, tf)
|
|
11
|
+
a = h(syminfo.tickerid, tfA)
|
|
12
|
+
b = h(syminfo.tickerid, tfB)
|
|
13
|
+
|
|
14
|
+
The analyzer resolves a helper's timeframe from the arguments of the helper's
|
|
15
|
+
own call sites only (``Analyzer._check_mixed_callsite_security_tf``), so the
|
|
16
|
+
codegen registered every such request on the chart's timeframe
|
|
17
|
+
(``input_tf_``) with no diagnostic, and ``h(ticker.heikinashi(...), tf)``
|
|
18
|
+
read plain candles. This pass resolves the symbol and timeframe of every
|
|
19
|
+
request whose context depends on a helper's parameters or locals along every
|
|
20
|
+
call path from the top level, with each parameter's value read in its
|
|
21
|
+
caller's scope, down to expressions of globals, inputs and built-ins, which
|
|
22
|
+
registration can evaluate:
|
|
23
|
+
|
|
24
|
+
* one context on every path: the request is annotated with it
|
|
25
|
+
(``annotations["pf_security_context"]``, read by the analyzer);
|
|
26
|
+
* several: each helper on the paths is copied once per distinct context it
|
|
27
|
+
leads to (``h__pfctx1``, ...), each copy calling the copies of its callees
|
|
28
|
+
that lead to its own contexts, and each request copy annotated with its
|
|
29
|
+
context;
|
|
30
|
+
* a value no registration can compute (a series, a reassigned local, a user
|
|
31
|
+
call, too many paths) is refused, naming the request, the parameter and the
|
|
32
|
+
path.
|
|
33
|
+
|
|
34
|
+
A request whose timeframe and symbol are bare parameters, bound at every call
|
|
35
|
+
site of its helper to a global expression, stays with the analyzer's call-site
|
|
36
|
+
clones (``callsite_idx``), which emit it as every earlier build did. A helper
|
|
37
|
+
no top-level statement reaches is marked dead: its requests never run and
|
|
38
|
+
keep the chart timeframe.
|
|
39
|
+
|
|
40
|
+
The helper's parameters a request's payload reads are part of its context
|
|
41
|
+
too: the payload is evaluated on the requested bars, where the argument a
|
|
42
|
+
parameter is bound to is recomputed::
|
|
43
|
+
|
|
44
|
+
nr(_s, _tf, _e) => request.security(_s, _tf, _e[1], lookahead = barmerge.lookahead_on)
|
|
45
|
+
reso(_x, _r) => nr(syminfo.tickerid, _r, _x)
|
|
46
|
+
a = reso(fastMa, "60")
|
|
47
|
+
b = reso(slowMa, "60")
|
|
48
|
+
|
|
49
|
+
The evaluator never bound such a parameter: ``_e[1]`` read a member nothing
|
|
50
|
+
pushed (``na`` on every bar), a bare ``_e`` was refused as an unknown
|
|
51
|
+
variable, and ``f(10) + f(20)`` with ``ta.sma(close, len)`` shared one
|
|
52
|
+
evaluator built from the first call's length. Each such parameter is
|
|
53
|
+
resolved like the symbol and the timeframe, bar series and user calls
|
|
54
|
+
allowed, the requests are copied once per distinct value, and each copy's
|
|
55
|
+
payload reads the value in the parameter's place (``fastMa[1]``; an untitled
|
|
56
|
+
input carries its declaration's key as its title). A length or history index
|
|
57
|
+
every top-level call passes one value keeps the analyzer's single evaluator;
|
|
58
|
+
its call-site clones for differing timeframes built every clone from the
|
|
59
|
+
first call's length. Only a value the request builder lowers on the
|
|
60
|
+
requested bars in the read's place is put in (``_Lowered``); any other keeps
|
|
61
|
+
the earlier lowering, never a refusal: a reassigned or ``var`` name, a loop
|
|
62
|
+
variable, a name the helper declares, a user call other than a global's
|
|
63
|
+
value the builder inlines whole (``_inlined_function``), a global declared
|
|
64
|
+
after the helper (read on the chart's terms there), a history object other
|
|
65
|
+
than a bar or price series, a ``ta.*`` call, an ``input.source``, an inline
|
|
66
|
+
operator expression or a global holding a numeric one or such a call, a
|
|
67
|
+
global under a builtin rendered on the chart's terms.
|
|
68
|
+
|
|
69
|
+
A request of another symbol that reads that symbol's pinned feed
|
|
70
|
+
(``external_requests``: the support checker's ``feed`` lowering) is keyed by
|
|
71
|
+
its symbol as well, so this pass owns every one whose symbol or timeframe
|
|
72
|
+
reaches it through a helper's parameters, the analyzer's call-site clones
|
|
73
|
+
included (they tell call sites apart by their timeframe only)::
|
|
74
|
+
|
|
75
|
+
f_htfPack(sym, tf) => request.security(sym, tf, f_packConfirmed())
|
|
76
|
+
f_symbolState(sym, tf) => f_htfPack(sym, tf)
|
|
77
|
+
f_tfRegime(sym) => [f_symbolState(sym, mainTf), f_symbolState(sym, confirmTf)]
|
|
78
|
+
[v1, v2] = f_tfRegime(vixSymbol)
|
|
79
|
+
[d1, d2] = f_tfRegime(dxySymbol)
|
|
80
|
+
|
|
81
|
+
is six contexts. Registration reads a symbol before the first bar, so one
|
|
82
|
+
that is not a value registration computes there on some call path (a
|
|
83
|
+
series, a reassigned name, a user call: ``ScriptIndex.registration_value``)
|
|
84
|
+
does not refuse the script: that request keeps the lowering it had before
|
|
85
|
+
it read a feed, a deferred refusal whose first read stops the run
|
|
86
|
+
(``external_requests.unpin_requests``), with a warning naming the path.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
from __future__ import annotations
|
|
90
|
+
|
|
91
|
+
import copy
|
|
92
|
+
from dataclasses import replace
|
|
93
|
+
|
|
94
|
+
from .ast_nodes import (
|
|
95
|
+
ArgOrder, ASTNode, Assignment, BinOp, BoolLiteral, ColorLiteral, ExprStmt, ForInStmt,
|
|
96
|
+
ForStmt, FuncCall, FuncDef, Identifier, IfStmt, MemberAccess, MethodDef,
|
|
97
|
+
NaLiteral, NumberLiteral, Program, StringLiteral, Subscript, SwitchStmt,
|
|
98
|
+
Ternary, TupleAssign, TupleLiteral, UnaryOp, VarDecl, WhileStmt,
|
|
99
|
+
)
|
|
100
|
+
from .errors import CompileError, Diagnostic, Level, Phase, SourceLocation
|
|
101
|
+
from .pine_spelling import input_binding_names
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
CONTEXT_ANNOTATION = "pf_security_context"
|
|
105
|
+
DEAD_ANNOTATION = "pf_security_dead"
|
|
106
|
+
# On a payload's read of a parameter of a helper no top-level statement
|
|
107
|
+
# reaches: the codegen does not warn that it reads na.
|
|
108
|
+
UNREACHED_ANNOTATION = "pf_security_unreached"
|
|
109
|
+
# On the Program: the warnings of the passes that run between the support
|
|
110
|
+
# checker and the analyzer, which the analyzer reports as its own.
|
|
111
|
+
PASS_WARNINGS_ANNOTATION = "pf_pass_warnings"
|
|
112
|
+
# ``external_requests.LOWERING_ANNOTATION`` and its ``feed`` lowering, spelled
|
|
113
|
+
# here too: that module imports this one.
|
|
114
|
+
_LOWERING_ANNOTATION = "pf_request_lowering"
|
|
115
|
+
_FEED_LOWERING = "feed"
|
|
116
|
+
_FEED_WARNING_ANNOTATION = "pf_request_feed_warning"
|
|
117
|
+
# On a global's read a payload parameter's value put in a helper's copy:
|
|
118
|
+
# the global may be declared after the helper, where the analyzer binds
|
|
119
|
+
# no name, and the codegen reads it as the global all the same.
|
|
120
|
+
GLOBAL_ANNOTATION = "pf_security_global"
|
|
121
|
+
_REQUEST_FUNCS = ("security", "security_lower_tf")
|
|
122
|
+
# Instances (a helper under one set of parameter values) the pass may build
|
|
123
|
+
# before it refuses the script: diamond-shaped helper graphs multiply paths.
|
|
124
|
+
_MAX_INSTANCES = 512
|
|
125
|
+
# Nodes a payload parameter's value may hold: a value doubling through nested
|
|
126
|
+
# helpers (``h(x) => g(x + x)``) keeps the earlier lowering.
|
|
127
|
+
_MAX_PAYLOAD_NODES = 256
|
|
128
|
+
_LITERALS = (StringLiteral, NumberLiteral, BoolLiteral, NaLiteral, ColorLiteral)
|
|
129
|
+
# The chart's own symbol strings, and the inputs, registration reads
|
|
130
|
+
# (``ScriptIndex.registration_value``).
|
|
131
|
+
_REGISTRATION_MEMBERS = frozenset({
|
|
132
|
+
("syminfo", "tickerid"), ("syminfo", "ticker"), ("syminfo", "prefix"),
|
|
133
|
+
("syminfo", "currency"), ("syminfo", "basecurrency"),
|
|
134
|
+
})
|
|
135
|
+
_REGISTRATION_INPUTS = frozenset({"symbol", "string", "bool"})
|
|
136
|
+
# Built-in series a context cannot be computed from before the first bar.
|
|
137
|
+
_BAR_SERIES = frozenset({
|
|
138
|
+
"open", "high", "low", "close", "volume", "hl2", "hlc3", "ohlc4", "hlcc4",
|
|
139
|
+
"time", "time_close", "time_tradingday", "timenow", "bar_index", "last_bar_index",
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class _Unresolvable(Exception):
|
|
144
|
+
"""A context value no registration can compute."""
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class _Fallback(Exception):
|
|
148
|
+
"""Requests whose payload parameters keep the earlier lowering."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, requests) -> None:
|
|
151
|
+
super().__init__()
|
|
152
|
+
self.requests = set(requests)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class _FeedFallback(Exception):
|
|
156
|
+
"""Requests of another symbol that read no feed: request id -> why."""
|
|
157
|
+
|
|
158
|
+
def __init__(self, reasons: dict[int, str]) -> None:
|
|
159
|
+
super().__init__()
|
|
160
|
+
self.reasons = dict(reasons)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _is_feed(request) -> bool:
|
|
164
|
+
"""``request`` reads another symbol's pinned feed."""
|
|
165
|
+
return (request.annotations or {}).get(_LOWERING_ANNOTATION) == _FEED_LOWERING
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def reads_bar_series(expr) -> str | None:
|
|
169
|
+
"""The built-in bar series ``expr`` reads, if any: a context computed
|
|
170
|
+
from it has no value before the first bar."""
|
|
171
|
+
for node in _walk(expr):
|
|
172
|
+
if isinstance(node, Identifier) and node.name in _BAR_SERIES:
|
|
173
|
+
return node.name
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _request_name(node) -> str | None:
|
|
178
|
+
if (isinstance(node, FuncCall) and isinstance(node.callee, MemberAccess)
|
|
179
|
+
and isinstance(node.callee.object, Identifier)
|
|
180
|
+
and node.callee.object.name == "request"
|
|
181
|
+
and node.callee.member in _REQUEST_FUNCS):
|
|
182
|
+
return node.callee.member
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def ticker_symbol_arg(node: FuncCall):
|
|
187
|
+
"""The symbol argument of ``ticker.inherit`` / ``standard`` /
|
|
188
|
+
``heikinashi``: ``ticker.inherit(from_tickerid, symbol)`` names the
|
|
189
|
+
symbol second (a one-argument spelling keeps its only one)."""
|
|
190
|
+
if "symbol" in node.kwargs:
|
|
191
|
+
return node.kwargs["symbol"]
|
|
192
|
+
if (isinstance(node.callee, MemberAccess) and node.callee.member == "inherit"
|
|
193
|
+
and len(node.args) > 1):
|
|
194
|
+
return node.args[1]
|
|
195
|
+
return node.args[0] if node.args else node.kwargs.get("from_tickerid")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _request_args(node: FuncCall) -> tuple[object, object]:
|
|
199
|
+
symbol = node.args[0] if node.args else node.kwargs.get("symbol")
|
|
200
|
+
tf = node.args[1] if len(node.args) > 1 else node.kwargs.get("timeframe")
|
|
201
|
+
return symbol, tf
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _request_payload(node: FuncCall):
|
|
205
|
+
"""The expression a request evaluates on the requested bars."""
|
|
206
|
+
return node.args[2] if len(node.args) > 2 else node.kwargs.get("expression")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _children(node):
|
|
210
|
+
for key, value in vars(node).items():
|
|
211
|
+
if key in ("loc", "annotations"):
|
|
212
|
+
continue
|
|
213
|
+
yield value
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _walk(value):
|
|
217
|
+
"""Every AST node below ``value``, depth first, in source order."""
|
|
218
|
+
stack = [value]
|
|
219
|
+
while stack:
|
|
220
|
+
item = stack.pop()
|
|
221
|
+
if isinstance(item, ASTNode):
|
|
222
|
+
yield item
|
|
223
|
+
stack.extend(reversed(list(_children(item))))
|
|
224
|
+
elif isinstance(item, (list, tuple)):
|
|
225
|
+
stack.extend(reversed(item))
|
|
226
|
+
elif isinstance(item, dict):
|
|
227
|
+
stack.extend(reversed(list(item.values())))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def replace_nodes(root, swaps: dict[int, ASTNode]) -> None:
|
|
231
|
+
"""Put ``swaps[id(node)]`` in the place of each such node (a call's
|
|
232
|
+
argument order too)."""
|
|
233
|
+
def fix(value):
|
|
234
|
+
if isinstance(value, ASTNode):
|
|
235
|
+
if id(value) in swaps:
|
|
236
|
+
return swaps[id(value)]
|
|
237
|
+
for key, item in list(vars(value).items()):
|
|
238
|
+
if key == "loc":
|
|
239
|
+
continue
|
|
240
|
+
if key == "annotations":
|
|
241
|
+
order = (item or {}).get("call_arg_order")
|
|
242
|
+
if isinstance(order, ArgOrder) and any(id(n) in swaps for n in order):
|
|
243
|
+
item["call_arg_order"] = ArgOrder(swaps.get(id(n), n) for n in order)
|
|
244
|
+
continue
|
|
245
|
+
new = fix(item)
|
|
246
|
+
if new is not item:
|
|
247
|
+
setattr(value, key, new)
|
|
248
|
+
return value
|
|
249
|
+
if isinstance(value, list):
|
|
250
|
+
for i, item in enumerate(value):
|
|
251
|
+
new = fix(item)
|
|
252
|
+
if new is not item:
|
|
253
|
+
value[i] = new
|
|
254
|
+
return value
|
|
255
|
+
if isinstance(value, tuple):
|
|
256
|
+
items = tuple(fix(item) for item in value)
|
|
257
|
+
return items if any(a is not b for a, b in zip(items, value)) else value
|
|
258
|
+
if isinstance(value, dict):
|
|
259
|
+
for key, item in list(value.items()):
|
|
260
|
+
new = fix(item)
|
|
261
|
+
if new is not item:
|
|
262
|
+
value[key] = new
|
|
263
|
+
return value
|
|
264
|
+
return value
|
|
265
|
+
|
|
266
|
+
fix(root)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def context_key(value):
|
|
270
|
+
"""A hashable spelling of an AST value, blind to locations."""
|
|
271
|
+
if isinstance(value, ASTNode):
|
|
272
|
+
return (type(value).__name__,) + tuple(
|
|
273
|
+
(key, context_key(item)) for key, item in sorted(vars(value).items())
|
|
274
|
+
if key not in ("loc", "annotations"))
|
|
275
|
+
if isinstance(value, (list, tuple)):
|
|
276
|
+
return tuple(context_key(item) for item in value)
|
|
277
|
+
if isinstance(value, dict):
|
|
278
|
+
return tuple(sorted((key, context_key(item)) for key, item in value.items()))
|
|
279
|
+
return value
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _spell(node) -> str:
|
|
283
|
+
"""A short Pine spelling of a resolved context value, for messages."""
|
|
284
|
+
if isinstance(node, StringLiteral):
|
|
285
|
+
return f'"{node.value}"'
|
|
286
|
+
if isinstance(node, Identifier):
|
|
287
|
+
return node.name
|
|
288
|
+
if isinstance(node, MemberAccess):
|
|
289
|
+
return f"{_spell(node.object)}.{node.member}"
|
|
290
|
+
if isinstance(node, FuncCall):
|
|
291
|
+
args = [_spell(a) for a in node.args]
|
|
292
|
+
args += [f"{k}={_spell(v)}" for k, v in node.kwargs.items()]
|
|
293
|
+
return f"{_spell(node.callee)}({', '.join(args)})"
|
|
294
|
+
if isinstance(node, NumberLiteral):
|
|
295
|
+
return str(node.value)
|
|
296
|
+
if isinstance(node, BoolLiteral):
|
|
297
|
+
return "true" if node.value else "false"
|
|
298
|
+
if isinstance(node, NaLiteral):
|
|
299
|
+
return "na"
|
|
300
|
+
if isinstance(node, BinOp):
|
|
301
|
+
return f"{_spell(node.left)} {node.op} {_spell(node.right)}"
|
|
302
|
+
if isinstance(node, UnaryOp):
|
|
303
|
+
return f"{node.op} {_spell(node.operand)}" if node.op == "not" else (
|
|
304
|
+
f"{node.op}{_spell(node.operand)}")
|
|
305
|
+
if isinstance(node, Ternary):
|
|
306
|
+
return (f"{_spell(node.condition)} ? {_spell(node.true_val)} : "
|
|
307
|
+
f"{_spell(node.false_val)}")
|
|
308
|
+
if isinstance(node, Subscript):
|
|
309
|
+
return f"{_spell(node.object)}[{_spell(node.index)}]"
|
|
310
|
+
return type(node).__name__
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class ScriptIndex:
|
|
314
|
+
"""The lexical bindings, helpers and calls of a program."""
|
|
315
|
+
|
|
316
|
+
def __init__(self, program: Program) -> None:
|
|
317
|
+
self.program = program
|
|
318
|
+
self.funcs: dict[str, FuncDef] = {}
|
|
319
|
+
# Names defined more than once: Pine overloads a function by its
|
|
320
|
+
# parameters, which this pass does not tell apart.
|
|
321
|
+
self.overloaded: set[str] = set()
|
|
322
|
+
for stmt in program.body:
|
|
323
|
+
if isinstance(stmt, FuncDef):
|
|
324
|
+
if stmt.name in self.funcs:
|
|
325
|
+
self.overloaded.add(stmt.name)
|
|
326
|
+
self.funcs[stmt.name] = stmt
|
|
327
|
+
# Identifier id -> its binding: ("global", name) for an unnested
|
|
328
|
+
# top-level declaration, ("param", func, name), ("local", decl id)
|
|
329
|
+
# for a declaration in a callable or a top-level block, ("bound",
|
|
330
|
+
# node id, name) for a loop variable or a tuple element. Built-ins
|
|
331
|
+
# and callables have none.
|
|
332
|
+
self.refs: dict[int, tuple] = {}
|
|
333
|
+
self.decls: dict[tuple, VarDecl] = {}
|
|
334
|
+
# Declaration (VarDecl, or TupleAssign and name) id -> the binding
|
|
335
|
+
# its name's reads resolve to in its block.
|
|
336
|
+
self.decl_binding: dict[tuple, tuple] = {}
|
|
337
|
+
self.unstable: set[tuple] = set() # reassigned, var or varip
|
|
338
|
+
self.reassigned: set[tuple] = set()
|
|
339
|
+
# FuncCall id -> the callable whose body holds it (None: top level).
|
|
340
|
+
self.owner: dict[int, str | None] = {}
|
|
341
|
+
self.calls: dict[str, list[FuncCall]] = {name: [] for name in self.funcs}
|
|
342
|
+
self.call_ids: set[int] = set() # the calls in ``calls``
|
|
343
|
+
self.requests: dict[str | None, list[FuncCall]] = {}
|
|
344
|
+
self._index()
|
|
345
|
+
# Global input call id -> the declaration naming it (its key when
|
|
346
|
+
# untitled).
|
|
347
|
+
self.input_names = input_binding_names(program.body)
|
|
348
|
+
# Every name a top-level statement declares.
|
|
349
|
+
self.program_names = {stmt.name for stmt in program.body
|
|
350
|
+
if isinstance(stmt, VarDecl) and stmt.name}
|
|
351
|
+
self.program_names.update(name for stmt in program.body
|
|
352
|
+
if isinstance(stmt, TupleAssign) for name in stmt.names)
|
|
353
|
+
|
|
354
|
+
def _index(self) -> None:
|
|
355
|
+
def resolve(name: str, scopes) -> tuple | None:
|
|
356
|
+
for scope in reversed(scopes):
|
|
357
|
+
if name in scope:
|
|
358
|
+
return scope[name]
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
def block(stmts, scopes, owner, binders=None, top=False) -> None:
|
|
362
|
+
scope = dict(binders or {})
|
|
363
|
+
for stmt in stmts:
|
|
364
|
+
if isinstance(stmt, VarDecl) and stmt.name:
|
|
365
|
+
key = ("global", stmt.name) if top else ("local", id(stmt))
|
|
366
|
+
scope.setdefault(stmt.name, key)
|
|
367
|
+
self.decls.setdefault(key, stmt)
|
|
368
|
+
self.decl_binding[(id(stmt), stmt.name)] = scope[stmt.name]
|
|
369
|
+
if stmt.is_var or stmt.is_varip:
|
|
370
|
+
self.unstable.add(key)
|
|
371
|
+
elif isinstance(stmt, TupleAssign):
|
|
372
|
+
for name in stmt.names:
|
|
373
|
+
scope.setdefault(
|
|
374
|
+
name, ("global", name) if top else ("bound", id(stmt), name))
|
|
375
|
+
self.decl_binding[(id(stmt), name)] = scope[name]
|
|
376
|
+
nested = (*scopes, scope)
|
|
377
|
+
for stmt in stmts:
|
|
378
|
+
walk(stmt, nested, owner)
|
|
379
|
+
|
|
380
|
+
def walk(value, scopes, owner) -> None:
|
|
381
|
+
if isinstance(value, (list, tuple)):
|
|
382
|
+
for item in value:
|
|
383
|
+
walk(item, scopes, owner)
|
|
384
|
+
return
|
|
385
|
+
if isinstance(value, dict):
|
|
386
|
+
for item in value.values():
|
|
387
|
+
walk(item, scopes, owner)
|
|
388
|
+
return
|
|
389
|
+
if not isinstance(value, ASTNode):
|
|
390
|
+
return
|
|
391
|
+
node = value
|
|
392
|
+
if isinstance(node, Identifier):
|
|
393
|
+
binding = resolve(node.name, scopes)
|
|
394
|
+
if binding is not None:
|
|
395
|
+
self.refs[id(node)] = binding
|
|
396
|
+
return
|
|
397
|
+
if isinstance(node, FuncCall):
|
|
398
|
+
self.owner[id(node)] = owner
|
|
399
|
+
if (isinstance(node.callee, Identifier) and node.callee.name in self.funcs
|
|
400
|
+
and resolve(node.callee.name, scopes) is None):
|
|
401
|
+
self.calls[node.callee.name].append(node)
|
|
402
|
+
self.call_ids.add(id(node))
|
|
403
|
+
if _request_name(node):
|
|
404
|
+
self.requests.setdefault(owner, []).append(node)
|
|
405
|
+
if isinstance(node, Assignment):
|
|
406
|
+
if isinstance(node.target, Identifier):
|
|
407
|
+
binding = resolve(node.target.name, scopes)
|
|
408
|
+
if binding is not None:
|
|
409
|
+
self.unstable.add(binding)
|
|
410
|
+
self.reassigned.add(binding)
|
|
411
|
+
if isinstance(node, (FuncDef, MethodDef)):
|
|
412
|
+
name = node.name if isinstance(node, FuncDef) else None
|
|
413
|
+
params = {p: ("param", node.name, p) for p in node.params}
|
|
414
|
+
block(node.body, scopes, name if name in self.funcs else f"method {node.name}",
|
|
415
|
+
params)
|
|
416
|
+
return
|
|
417
|
+
if isinstance(node, IfStmt):
|
|
418
|
+
walk(node.condition, scopes, owner)
|
|
419
|
+
block(node.body, scopes, owner)
|
|
420
|
+
block(node.else_body, scopes, owner)
|
|
421
|
+
return
|
|
422
|
+
if isinstance(node, ForStmt):
|
|
423
|
+
walk([node.start, node.end, node.step], scopes, owner)
|
|
424
|
+
block(node.body, scopes, owner, {node.var: ("bound", id(node), node.var)})
|
|
425
|
+
return
|
|
426
|
+
if isinstance(node, ForInStmt):
|
|
427
|
+
walk(node.iterable, scopes, owner)
|
|
428
|
+
names = [node.var] if node.var else list(node.vars or ())
|
|
429
|
+
block(node.body, scopes, owner,
|
|
430
|
+
{n: ("bound", id(node), n) for n in names})
|
|
431
|
+
return
|
|
432
|
+
if isinstance(node, WhileStmt):
|
|
433
|
+
walk(node.condition, scopes, owner)
|
|
434
|
+
block(node.body, scopes, owner)
|
|
435
|
+
return
|
|
436
|
+
if isinstance(node, SwitchStmt):
|
|
437
|
+
walk(node.expr, scopes, owner)
|
|
438
|
+
for case_expr, body in node.cases:
|
|
439
|
+
walk(case_expr, scopes, owner)
|
|
440
|
+
block(body, scopes, owner)
|
|
441
|
+
block(node.default_body, scopes, owner)
|
|
442
|
+
return
|
|
443
|
+
for child in _children(node):
|
|
444
|
+
walk(child, scopes, owner)
|
|
445
|
+
|
|
446
|
+
block(self.program.body, (), None, top=True)
|
|
447
|
+
|
|
448
|
+
# -- scopes --------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
def depends_on_scope(self, expr) -> bool:
|
|
451
|
+
"""``expr`` reads a parameter, a local or a bound name."""
|
|
452
|
+
return any(isinstance(n, Identifier) and self.refs.get(id(n), ("global",))[0]
|
|
453
|
+
in ("param", "local", "bound") for n in _walk(expr))
|
|
454
|
+
|
|
455
|
+
def param_arg(self, func: str, call: FuncCall, param: str):
|
|
456
|
+
"""The argument ``call`` binds to ``param`` of ``func`` (its default
|
|
457
|
+
when omitted), or None."""
|
|
458
|
+
fdef = self.funcs[func]
|
|
459
|
+
if param in call.kwargs:
|
|
460
|
+
return call.kwargs[param]
|
|
461
|
+
index = fdef.params.index(param)
|
|
462
|
+
if index < len(call.args):
|
|
463
|
+
return call.args[index]
|
|
464
|
+
defaults = (fdef.annotations or {}).get("param_defaults") or []
|
|
465
|
+
return defaults[index] if index < len(defaults) else None
|
|
466
|
+
|
|
467
|
+
# -- values --------------------------------------------------------------
|
|
468
|
+
|
|
469
|
+
def resolve(self, expr, env: dict | None, seen: frozenset = frozenset()):
|
|
470
|
+
"""``expr``, read in a scope whose parameters ``env`` binds, as an
|
|
471
|
+
expression of globals, inputs, literals and built-ins."""
|
|
472
|
+
if isinstance(expr, _LITERALS):
|
|
473
|
+
return expr
|
|
474
|
+
if isinstance(expr, Identifier):
|
|
475
|
+
binding = self.refs.get(id(expr))
|
|
476
|
+
if binding is None and expr.name in _BAR_SERIES:
|
|
477
|
+
raise _Unresolvable(f"it reads '{expr.name}', a series")
|
|
478
|
+
if binding is None or binding[0] == "global":
|
|
479
|
+
return expr
|
|
480
|
+
if binding[0] == "param":
|
|
481
|
+
if env is None or binding[2] not in env:
|
|
482
|
+
raise _Unresolvable(f"parameter '{binding[2]}' has no argument")
|
|
483
|
+
arg, caller_env = env[binding[2]]
|
|
484
|
+
if arg is None:
|
|
485
|
+
raise _Unresolvable(f"parameter '{binding[2]}' has no argument")
|
|
486
|
+
return self.resolve(arg, caller_env, seen)
|
|
487
|
+
if binding[0] == "local" and binding not in self.unstable and binding not in seen:
|
|
488
|
+
decl = self.decls[binding]
|
|
489
|
+
if decl.value is not None:
|
|
490
|
+
return self.resolve(decl.value, env, seen | {binding})
|
|
491
|
+
raise _Unresolvable(f"'{expr.name}' is not a value fixed before the first bar")
|
|
492
|
+
if isinstance(expr, MemberAccess):
|
|
493
|
+
if (isinstance(expr.object, Identifier)
|
|
494
|
+
and self.refs.get(id(expr.object), ("global",))[0] == "global"):
|
|
495
|
+
return expr
|
|
496
|
+
raise _Unresolvable(f"'{_spell(expr)}' is not a value fixed before the first bar")
|
|
497
|
+
if isinstance(expr, FuncCall):
|
|
498
|
+
if isinstance(expr.callee, Identifier) and expr.callee.name in self.funcs:
|
|
499
|
+
raise _Unresolvable(f"the user function call {expr.callee.name}(...)")
|
|
500
|
+
if not (isinstance(expr.callee, Identifier) or (
|
|
501
|
+
isinstance(expr.callee, MemberAccess)
|
|
502
|
+
and isinstance(expr.callee.object, Identifier)
|
|
503
|
+
and id(expr.callee.object) not in self.refs)):
|
|
504
|
+
raise _Unresolvable(f"the method call {_spell(expr)}")
|
|
505
|
+
args = [self.resolve(a, env, seen) for a in expr.args]
|
|
506
|
+
kwargs = {k: self.resolve(v, env, seen) for k, v in expr.kwargs.items()}
|
|
507
|
+
if all(a is b for a, b in zip(args, expr.args)) and all(
|
|
508
|
+
kwargs[k] is expr.kwargs[k] for k in kwargs):
|
|
509
|
+
return expr
|
|
510
|
+
return replace(expr, args=args, kwargs=kwargs)
|
|
511
|
+
if isinstance(expr, Ternary):
|
|
512
|
+
parts = [self.resolve(p, env, seen)
|
|
513
|
+
for p in (expr.condition, expr.true_val, expr.false_val)]
|
|
514
|
+
if all(a is b for a, b in zip(parts, (expr.condition, expr.true_val,
|
|
515
|
+
expr.false_val))):
|
|
516
|
+
return expr
|
|
517
|
+
return replace(expr, condition=parts[0], true_val=parts[1], false_val=parts[2])
|
|
518
|
+
if isinstance(expr, BinOp):
|
|
519
|
+
left, right = self.resolve(expr.left, env, seen), self.resolve(expr.right, env, seen)
|
|
520
|
+
if left is expr.left and right is expr.right:
|
|
521
|
+
return expr
|
|
522
|
+
return replace(expr, left=left, right=right)
|
|
523
|
+
if isinstance(expr, UnaryOp):
|
|
524
|
+
operand = self.resolve(expr.operand, env, seen)
|
|
525
|
+
return expr if operand is expr.operand else replace(expr, operand=operand)
|
|
526
|
+
raise _Unresolvable(f"a {type(expr).__name__} is not a value fixed before the first bar")
|
|
527
|
+
|
|
528
|
+
def resolve_payload(self, expr, env: dict | None, seen: frozenset = frozenset()):
|
|
529
|
+
"""A fresh copy of ``expr``, read in a scope whose parameters ``env``
|
|
530
|
+
binds, with each parameter and local replaced by its value: an
|
|
531
|
+
expression of globals, literals and built-ins, bar series and user
|
|
532
|
+
calls included, which a payload recomputes on the requested bars."""
|
|
533
|
+
swaps = {}
|
|
534
|
+
for node in _walk(expr):
|
|
535
|
+
if isinstance(node, FuncCall) and _request_name(node):
|
|
536
|
+
raise _Unresolvable(f"it holds a request.{_request_name(node)} call")
|
|
537
|
+
if not isinstance(node, Identifier):
|
|
538
|
+
continue
|
|
539
|
+
binding = self.refs.get(id(node))
|
|
540
|
+
if binding is None:
|
|
541
|
+
continue
|
|
542
|
+
if binding in self.unstable:
|
|
543
|
+
raise _Unresolvable(f"'{node.name}' is reassigned or declared var")
|
|
544
|
+
if binding[0] == "global":
|
|
545
|
+
continue
|
|
546
|
+
if binding[0] == "param":
|
|
547
|
+
arg, caller_env = (env or {}).get(binding[2], (None, None))
|
|
548
|
+
if arg is None:
|
|
549
|
+
raise _Unresolvable(f"parameter '{binding[2]}' has no argument")
|
|
550
|
+
swaps[id(node)] = self.resolve_payload(arg, caller_env, seen)
|
|
551
|
+
elif (binding[0] == "local" and binding not in seen
|
|
552
|
+
and self.decls[binding].value is not None):
|
|
553
|
+
swaps[id(node)] = self.resolve_payload(
|
|
554
|
+
self.decls[binding].value, env, seen | {binding})
|
|
555
|
+
else:
|
|
556
|
+
raise _Unresolvable(f"'{node.name}' is not a value the requested bars recompute")
|
|
557
|
+
if id(expr) in swaps:
|
|
558
|
+
return swaps[id(expr)]
|
|
559
|
+
memo: dict = {}
|
|
560
|
+
fresh = copy.deepcopy(expr, memo)
|
|
561
|
+
replace_nodes(fresh, {id(memo[key]): value for key, value in swaps.items()})
|
|
562
|
+
for node in _walk(expr):
|
|
563
|
+
if (isinstance(node, Identifier)
|
|
564
|
+
and self.refs.get(id(node), ("",))[0] == "global"):
|
|
565
|
+
read = memo[id(node)]
|
|
566
|
+
read.annotations = {**(read.annotations or {}), GLOBAL_ANNOTATION: True}
|
|
567
|
+
if sum(1 for _ in _walk(fresh)) > _MAX_PAYLOAD_NODES:
|
|
568
|
+
raise _Unresolvable("its value is too large")
|
|
569
|
+
# An untitled input is keyed by the declaration holding it, which its
|
|
570
|
+
# copy leaves: the copy carries the key as its title.
|
|
571
|
+
for node in _walk(expr):
|
|
572
|
+
name = self.input_names.get(id(node))
|
|
573
|
+
if name is None or "title" in node.kwargs or len(node.args) > 1:
|
|
574
|
+
continue
|
|
575
|
+
call = memo[id(node)]
|
|
576
|
+
title = StringLiteral(value=name)
|
|
577
|
+
call.kwargs = {**call.kwargs, "title": title}
|
|
578
|
+
order = (call.annotations or {}).get("call_arg_order")
|
|
579
|
+
if isinstance(order, ArgOrder):
|
|
580
|
+
call.annotations = {**call.annotations,
|
|
581
|
+
"call_arg_order": ArgOrder([*order, title])}
|
|
582
|
+
return fresh
|
|
583
|
+
|
|
584
|
+
def registers_timeframe(self, expr, seen: frozenset = frozenset()) -> bool:
|
|
585
|
+
"""``expr``, a request's timeframe, has the run's value where
|
|
586
|
+
registration reads it, before the first bar: it reads no bar series,
|
|
587
|
+
and no name the script reassigns or declares ``var`` (there it holds
|
|
588
|
+
its type's default), through globals too."""
|
|
589
|
+
if reads_bar_series(expr) is not None:
|
|
590
|
+
return False
|
|
591
|
+
for node in _walk(expr):
|
|
592
|
+
if not isinstance(node, Identifier):
|
|
593
|
+
continue
|
|
594
|
+
binding = self.refs.get(id(node))
|
|
595
|
+
if binding is None or binding in seen:
|
|
596
|
+
continue
|
|
597
|
+
if binding[0] != "global" or binding in self.unstable:
|
|
598
|
+
return False
|
|
599
|
+
decl = self.decls.get(binding)
|
|
600
|
+
if decl is not None and decl.value is not None and not self.registers_timeframe(
|
|
601
|
+
decl.value, seen | {binding}):
|
|
602
|
+
return False
|
|
603
|
+
return True
|
|
604
|
+
|
|
605
|
+
def registration_value(self, expr, seen: frozenset = frozenset()) -> bool:
|
|
606
|
+
"""``expr`` is a value registration computes before the first bar:
|
|
607
|
+
literals, inputs (TradingView takes constant arguments), the chart's
|
|
608
|
+
own symbol strings, and operators and ternaries over them, read
|
|
609
|
+
through globals never reassigned. A request of another symbol is
|
|
610
|
+
registered with its symbol string and ``ignore_invalid_symbol`` so."""
|
|
611
|
+
if isinstance(expr, (StringLiteral, NumberLiteral, BoolLiteral)):
|
|
612
|
+
return True
|
|
613
|
+
if isinstance(expr, Identifier):
|
|
614
|
+
binding = self.refs.get(id(expr))
|
|
615
|
+
if (binding is None or binding[0] != "global" or binding in self.unstable
|
|
616
|
+
or binding in seen):
|
|
617
|
+
return False
|
|
618
|
+
decl = self.decls.get(binding)
|
|
619
|
+
return (decl is not None and decl.value is not None
|
|
620
|
+
and self.registration_value(decl.value, seen | {binding}))
|
|
621
|
+
if isinstance(expr, MemberAccess):
|
|
622
|
+
return (isinstance(expr.object, Identifier) and id(expr.object) not in self.refs
|
|
623
|
+
and (expr.object.name, expr.member) in _REGISTRATION_MEMBERS)
|
|
624
|
+
if isinstance(expr, FuncCall):
|
|
625
|
+
callee = expr.callee
|
|
626
|
+
if isinstance(callee, Identifier):
|
|
627
|
+
return callee.name == "input" and id(callee) not in self.refs
|
|
628
|
+
if not (isinstance(callee, MemberAccess) and isinstance(callee.object, Identifier)
|
|
629
|
+
and id(callee.object) not in self.refs):
|
|
630
|
+
return False
|
|
631
|
+
if callee.object.name == "input":
|
|
632
|
+
return callee.member in _REGISTRATION_INPUTS
|
|
633
|
+
if callee.object.name == "ticker" and callee.member in ("inherit", "standard"):
|
|
634
|
+
symbol = ticker_symbol_arg(expr)
|
|
635
|
+
return symbol is not None and self.registration_value(symbol, seen)
|
|
636
|
+
return False
|
|
637
|
+
if isinstance(expr, BinOp):
|
|
638
|
+
return (expr.op in ("+", "==", "!=", "and", "or")
|
|
639
|
+
and self.registration_value(expr.left, seen)
|
|
640
|
+
and self.registration_value(expr.right, seen))
|
|
641
|
+
if isinstance(expr, UnaryOp):
|
|
642
|
+
return expr.op == "not" and self.registration_value(expr.operand, seen)
|
|
643
|
+
if isinstance(expr, Ternary):
|
|
644
|
+
return all(self.registration_value(part, seen)
|
|
645
|
+
for part in (expr.condition, expr.true_val, expr.false_val))
|
|
646
|
+
return False
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _error(node, message: str, filename: str) -> Diagnostic:
|
|
650
|
+
loc = getattr(node, "loc", None) or SourceLocation(file=filename, line=1, col=1, end_col=1)
|
|
651
|
+
return Diagnostic(level=Level.ERROR, phase=Phase.ANALYZER, location=loc, message=message,
|
|
652
|
+
hint=("Pass the symbol and timeframe as literals, inputs or globals "
|
|
653
|
+
"at the helper's call sites."))
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def specialize_security_contexts(program: Program, filename: str = "<input>") -> Program:
|
|
657
|
+
"""Resolve the context of every helper request (module docstring)."""
|
|
658
|
+
while True:
|
|
659
|
+
try:
|
|
660
|
+
return _specialize(program, filename)
|
|
661
|
+
except _FeedFallback as exc:
|
|
662
|
+
# Nothing is rewritten before the plan holds: these requests keep
|
|
663
|
+
# their deferred refusal, and the pass starts over without them.
|
|
664
|
+
from .external_requests import unpin_requests
|
|
665
|
+
unpin_requests(program, exc.reasons)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _specialize(program: Program, filename: str) -> Program:
|
|
669
|
+
prog = ScriptIndex(program)
|
|
670
|
+
_mark_unreached(prog, _reached(prog))
|
|
671
|
+
# The requests whose context this pass owns, by helper; those owned for
|
|
672
|
+
# their symbol or timeframe; the payload parameter reads of those owned
|
|
673
|
+
# for their payload.
|
|
674
|
+
owned: dict[str, list[FuncCall]] = {}
|
|
675
|
+
context_owned: set[int] = set()
|
|
676
|
+
payload_reads: dict[int, list[Identifier]] = {}
|
|
677
|
+
# Another symbol's request whose symbol or timeframe reads a name this
|
|
678
|
+
# pass does not resolve (a method's parameter, a block's local) reads no
|
|
679
|
+
# feed: the support checker keeps those, this is its backstop.
|
|
680
|
+
stray = {id(request): "its symbol or timeframe reads a name of a method or a block"
|
|
681
|
+
for owner, requests in prog.requests.items() if owner not in prog.funcs
|
|
682
|
+
for request in requests if _is_feed(request)
|
|
683
|
+
and any(prog.depends_on_scope(arg) for arg in _request_args(request))}
|
|
684
|
+
if stray:
|
|
685
|
+
raise _FeedFallback(stray)
|
|
686
|
+
for owner, requests in prog.requests.items():
|
|
687
|
+
if owner not in prog.funcs:
|
|
688
|
+
continue
|
|
689
|
+
for request in requests:
|
|
690
|
+
symbol, tf = _request_args(request)
|
|
691
|
+
if (prog.depends_on_scope(symbol) or prog.depends_on_scope(tf)) and (
|
|
692
|
+
_is_feed(request) or not (
|
|
693
|
+
_request_name(request) == "security"
|
|
694
|
+
and _analyzer_resolves(prog, owner, symbol, tf))):
|
|
695
|
+
context_owned.add(id(request))
|
|
696
|
+
reads = _payload_params(prog, owner, request)
|
|
697
|
+
if reads and not _analyzer_binds_payload(prog, owner, request, reads):
|
|
698
|
+
payload_reads[id(request)] = reads
|
|
699
|
+
if id(request) in context_owned or id(request) in payload_reads:
|
|
700
|
+
owned.setdefault(owner, []).append(request)
|
|
701
|
+
while True:
|
|
702
|
+
if not owned:
|
|
703
|
+
return program
|
|
704
|
+
try:
|
|
705
|
+
leads, instances, detail, top = _plan(
|
|
706
|
+
prog, owned, context_owned, payload_reads, filename)
|
|
707
|
+
break
|
|
708
|
+
except _Fallback as exc:
|
|
709
|
+
# These requests keep the earlier lowering of their payload (all
|
|
710
|
+
# of them, should none of these be owned for it).
|
|
711
|
+
dropped = exc.requests & payload_reads.keys()
|
|
712
|
+
for request_id in dropped or list(payload_reads):
|
|
713
|
+
payload_reads.pop(request_id)
|
|
714
|
+
owned = {func: kept for func, requests in owned.items()
|
|
715
|
+
if (kept := [r for r in requests if id(r) in context_owned
|
|
716
|
+
or id(r) in payload_reads])}
|
|
717
|
+
_type_context_params(prog, owned, leads)
|
|
718
|
+
|
|
719
|
+
# Every call path keys each request of another symbol this pass owns: the
|
|
720
|
+
# support checker's warning that it reads a feed now holds.
|
|
721
|
+
warnings = []
|
|
722
|
+
for requests in owned.values():
|
|
723
|
+
for request in requests:
|
|
724
|
+
notes = request.annotations or {}
|
|
725
|
+
if _FEED_WARNING_ANNOTATION in notes:
|
|
726
|
+
warnings.append(notes[_FEED_WARNING_ANNOTATION])
|
|
727
|
+
request.annotations = {k: v for k, v in notes.items()
|
|
728
|
+
if k != _FEED_WARNING_ANNOTATION}
|
|
729
|
+
if warnings:
|
|
730
|
+
pass_notes = program.annotations = dict(program.annotations or {})
|
|
731
|
+
pass_notes[PASS_WARNINGS_ANNOTATION] = [
|
|
732
|
+
*pass_notes.get(PASS_WARNINGS_ANNOTATION, ()), *warnings]
|
|
733
|
+
|
|
734
|
+
# A request no top-level statement reaches never runs: one owned for its
|
|
735
|
+
# context keeps the chart timeframe, and no read of a parameter of its
|
|
736
|
+
# helper warns.
|
|
737
|
+
for func, requests in owned.items():
|
|
738
|
+
if func not in instances:
|
|
739
|
+
for request in requests:
|
|
740
|
+
if id(request) in context_owned:
|
|
741
|
+
request.annotations = {**(request.annotations or {}),
|
|
742
|
+
DEAD_ANNOTATION: True}
|
|
743
|
+
|
|
744
|
+
# Name every instance and build its definition: a helper's first
|
|
745
|
+
# signature keeps its own, every other one is a copy of it.
|
|
746
|
+
taken = {n.name for n in _walk(program) if isinstance(n, Identifier)} | set(prog.funcs)
|
|
747
|
+
taken |= {name for n in _walk(program) for name in _declared_names(n)}
|
|
748
|
+
names: dict[tuple, str] = {}
|
|
749
|
+
defs: dict[tuple, tuple[FuncDef, dict | None]] = {}
|
|
750
|
+
new_defs: dict[str, list[FuncDef]] = {}
|
|
751
|
+
for func, sigs in instances.items():
|
|
752
|
+
original = prog.funcs[func]
|
|
753
|
+
for index, signature in enumerate(sigs):
|
|
754
|
+
if index == 0:
|
|
755
|
+
names[signature] = func
|
|
756
|
+
defs[signature] = (original, None)
|
|
757
|
+
continue
|
|
758
|
+
n = index
|
|
759
|
+
while f"{func}__pfctx{n}" in taken:
|
|
760
|
+
n += 1
|
|
761
|
+
names[signature] = f"{func}__pfctx{n}"
|
|
762
|
+
taken.add(names[signature])
|
|
763
|
+
memo: dict = {}
|
|
764
|
+
clone = copy.deepcopy(original, memo)
|
|
765
|
+
clone.name = names[signature]
|
|
766
|
+
_rename_locals(prog, original, memo, names[signature][len(func):], taken)
|
|
767
|
+
defs[signature] = (clone, memo)
|
|
768
|
+
new_defs.setdefault(func, []).append(clone)
|
|
769
|
+
|
|
770
|
+
for signature, (fdef, memo) in defs.items():
|
|
771
|
+
info = detail[signature]
|
|
772
|
+
mapped = (lambda node: node) if memo is None else (lambda node, m=memo: m[id(node)])
|
|
773
|
+
for request, (symbol, tf), payload in info["contexts"]:
|
|
774
|
+
target = mapped(request)
|
|
775
|
+
target.annotations = {
|
|
776
|
+
**(target.annotations or {}),
|
|
777
|
+
CONTEXT_ANNOTATION: {"symbol": _alias_value(prog, symbol), "timeframe": tf}}
|
|
778
|
+
if payload:
|
|
779
|
+
values = dict(payload)
|
|
780
|
+
replace_nodes(target, {
|
|
781
|
+
id(mapped(read)): copy.deepcopy(values[prog.refs[id(read)][2]])
|
|
782
|
+
for read in payload_reads[id(request)]})
|
|
783
|
+
for call, child in info["kids"]:
|
|
784
|
+
target = mapped(call)
|
|
785
|
+
target.callee = replace(target.callee, name=names[child])
|
|
786
|
+
for call, signature in top:
|
|
787
|
+
call.callee = replace(call.callee, name=names[signature])
|
|
788
|
+
|
|
789
|
+
body = []
|
|
790
|
+
for stmt in program.body:
|
|
791
|
+
body.append(stmt)
|
|
792
|
+
if isinstance(stmt, FuncDef):
|
|
793
|
+
body.extend(new_defs.get(stmt.name, ()))
|
|
794
|
+
program.body = body
|
|
795
|
+
return program
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _plan(prog: ScriptIndex, owned: dict, context_owned: set[int],
|
|
799
|
+
payload_reads: dict[int, list], filename: str):
|
|
800
|
+
"""The helpers leading to ``owned``, their instances, each instance's
|
|
801
|
+
contexts and child calls, and the top-level calls' instances. Refuses a
|
|
802
|
+
symbol or timeframe no registration can compute; raises ``_Fallback``
|
|
803
|
+
for payloads this pass cannot rewrite."""
|
|
804
|
+
# Every helper whose calls lead to an owned request.
|
|
805
|
+
leads = set(owned)
|
|
806
|
+
changed = True
|
|
807
|
+
while changed:
|
|
808
|
+
changed = False
|
|
809
|
+
for name in prog.funcs:
|
|
810
|
+
if name in leads:
|
|
811
|
+
continue
|
|
812
|
+
if any(prog.owner.get(id(c)) == name for g in leads for c in prog.calls[g]):
|
|
813
|
+
leads.add(name)
|
|
814
|
+
changed = True
|
|
815
|
+
|
|
816
|
+
errors: dict[int, Diagnostic] = {}
|
|
817
|
+
|
|
818
|
+
def refuse(request: FuncCall, message: str) -> None:
|
|
819
|
+
errors.setdefault(id(request), _error(request, message, filename))
|
|
820
|
+
|
|
821
|
+
def payload_only(funcs) -> list[int]:
|
|
822
|
+
return [id(r) for g in funcs for r in owned.get(g, ()) if id(r) not in context_owned]
|
|
823
|
+
|
|
824
|
+
context_first = {func: first for func, requests in owned.items()
|
|
825
|
+
if (first := next((r for r in requests if id(r) in context_owned), None))}
|
|
826
|
+
for func in sorted(leads):
|
|
827
|
+
if func in prog.overloaded:
|
|
828
|
+
problem = "is overloaded, and its overloads' call paths are not told apart"
|
|
829
|
+
else:
|
|
830
|
+
problem = next((f"is called from {owner}, whose calls this pass does not follow"
|
|
831
|
+
for call in prog.calls[func]
|
|
832
|
+
if (owner := prog.owner.get(id(call))) is not None
|
|
833
|
+
and owner not in prog.funcs), None)
|
|
834
|
+
if problem is None:
|
|
835
|
+
continue
|
|
836
|
+
reach = {func} | _reachable(prog, func, leads)
|
|
837
|
+
feeds = {id(r): f"helper '{func}' {problem}"
|
|
838
|
+
for g in reach for r in owned.get(g, ()) if _is_feed(r)}
|
|
839
|
+
if feeds:
|
|
840
|
+
raise _FeedFallback(feeds)
|
|
841
|
+
# The request a helper leads to, for the message.
|
|
842
|
+
target = context_first.get(func) or next(
|
|
843
|
+
(context_first[g] for g in sorted(context_first) if g in reach), None)
|
|
844
|
+
if target is None:
|
|
845
|
+
dropped = payload_only(reach) or list(payload_reads)
|
|
846
|
+
if dropped:
|
|
847
|
+
raise _Fallback(dropped)
|
|
848
|
+
# ``_reachable`` walks an overloaded helper's last definition.
|
|
849
|
+
target = next(r for requests in owned.values() for r in requests)
|
|
850
|
+
refuse(target, f"request.{_request_name(target)} context: helper '{func}' {problem}")
|
|
851
|
+
if errors:
|
|
852
|
+
raise CompileError(list(errors.values()))
|
|
853
|
+
|
|
854
|
+
instances: dict[str, list] = {} # helper -> distinct signatures, first seen first
|
|
855
|
+
detail: dict[tuple, dict] = {} # signature -> contexts and child calls
|
|
856
|
+
budget = [0]
|
|
857
|
+
|
|
858
|
+
def instantiate(func: str, env: dict, path: tuple) -> tuple:
|
|
859
|
+
budget[0] += 1
|
|
860
|
+
if budget[0] > _MAX_INSTANCES or func in {name for name, _ in path}:
|
|
861
|
+
raise _Unresolvable("the helpers reach it through too many call paths")
|
|
862
|
+
here = path + ((func, env),)
|
|
863
|
+
contexts = []
|
|
864
|
+
for request in owned.get(func, []):
|
|
865
|
+
symbol, tf = _request_args(request)
|
|
866
|
+
values = []
|
|
867
|
+
for what, arg in (("symbol", symbol), ("timeframe", tf)):
|
|
868
|
+
chain = " -> ".join(f"{name}()" for name, _ in here)
|
|
869
|
+
try:
|
|
870
|
+
values.append(prog.resolve(arg, env) if arg is not None else None)
|
|
871
|
+
except _Unresolvable as exc:
|
|
872
|
+
if id(request) not in context_owned:
|
|
873
|
+
raise _Fallback([id(request)]) from exc
|
|
874
|
+
if _is_feed(request):
|
|
875
|
+
raise _FeedFallback({id(request): (
|
|
876
|
+
f"its {what} cannot be resolved before the first bar on the "
|
|
877
|
+
f"call path {chain}: {exc}")}) from exc
|
|
878
|
+
refuse(request, f"request.{_request_name(request)} {what} cannot be "
|
|
879
|
+
f"resolved before the first bar on the call path "
|
|
880
|
+
f"{chain}: {exc}")
|
|
881
|
+
values.append(None)
|
|
882
|
+
continue
|
|
883
|
+
if (what == "symbol" and _is_feed(request) and values[-1] is not None
|
|
884
|
+
and not prog.registration_value(values[-1])):
|
|
885
|
+
raise _FeedFallback({id(request): (
|
|
886
|
+
f"its symbol {_spell(values[-1])} on the call path {chain} is not "
|
|
887
|
+
"a value registration computes before the first bar")})
|
|
888
|
+
if (what == "timeframe" and _is_feed(request) and values[-1] is not None
|
|
889
|
+
and not prog.registers_timeframe(values[-1])):
|
|
890
|
+
raise _FeedFallback({id(request): (
|
|
891
|
+
f"its timeframe {_spell(values[-1])} on the call path {chain} is "
|
|
892
|
+
"not a value registration computes before the first bar")})
|
|
893
|
+
payload = ()
|
|
894
|
+
if id(request) in payload_reads:
|
|
895
|
+
try:
|
|
896
|
+
payload = _payload_values(prog, func, request, payload_reads[id(request)],
|
|
897
|
+
env)
|
|
898
|
+
except _Unresolvable as exc:
|
|
899
|
+
raise _Fallback([id(request)]) from exc
|
|
900
|
+
contexts.append((request, tuple(values), payload))
|
|
901
|
+
kids = []
|
|
902
|
+
for call in _body_calls(prog, func, leads):
|
|
903
|
+
callee = call.callee.name
|
|
904
|
+
callee_env = {p: (prog.param_arg(callee, call, p), env)
|
|
905
|
+
for p in prog.funcs[callee].params}
|
|
906
|
+
kids.append((call, instantiate(callee, callee_env, here)))
|
|
907
|
+
signature = (func, tuple(context_key((v, payload)) for _, v, payload in contexts),
|
|
908
|
+
tuple(sig for _, sig in kids))
|
|
909
|
+
if signature not in detail:
|
|
910
|
+
detail[signature] = {"contexts": contexts, "kids": kids}
|
|
911
|
+
instances.setdefault(func, []).append(signature)
|
|
912
|
+
return signature
|
|
913
|
+
|
|
914
|
+
top: list[tuple[FuncCall, tuple]] = []
|
|
915
|
+
try:
|
|
916
|
+
for name in sorted(leads, key=lambda n: prog.program.body.index(prog.funcs[n])):
|
|
917
|
+
for call in prog.calls[name]:
|
|
918
|
+
if prog.owner.get(id(call)) is not None:
|
|
919
|
+
continue
|
|
920
|
+
env = {p: (prog.param_arg(name, call, p), None)
|
|
921
|
+
for p in prog.funcs[name].params}
|
|
922
|
+
top.append((call, instantiate(name, env, ())))
|
|
923
|
+
except _Unresolvable as exc:
|
|
924
|
+
feeds = {id(r): str(exc) for requests in owned.values() for r in requests
|
|
925
|
+
if _is_feed(r)}
|
|
926
|
+
if feeds:
|
|
927
|
+
raise _FeedFallback(feeds) from exc
|
|
928
|
+
if payload_reads:
|
|
929
|
+
raise _Fallback(list(payload_reads)) from exc
|
|
930
|
+
request = next(iter(owned.values()))[0]
|
|
931
|
+
refuse(request, f"request.{_request_name(request)} context: {exc}")
|
|
932
|
+
if errors:
|
|
933
|
+
raise CompileError(list(errors.values()))
|
|
934
|
+
return leads, instances, detail, top
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def _declared_names(node) -> list[str]:
|
|
938
|
+
"""The names a declaration or a loop binds."""
|
|
939
|
+
if isinstance(node, VarDecl):
|
|
940
|
+
return [node.name] if node.name else []
|
|
941
|
+
if isinstance(node, TupleAssign):
|
|
942
|
+
return list(node.names)
|
|
943
|
+
if isinstance(node, ForStmt):
|
|
944
|
+
return [node.var]
|
|
945
|
+
if isinstance(node, ForInStmt):
|
|
946
|
+
return [node.var] if node.var else list(node.vars or ())
|
|
947
|
+
return []
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _rename_locals(prog: ScriptIndex, original: FuncDef, memo: dict, suffix: str,
|
|
951
|
+
taken: set[str]) -> None:
|
|
952
|
+
"""Give a helper copy's locals and loop variables names of their own
|
|
953
|
+
(``v`` becomes ``v__pfctx1``): the analyzer keeps a callable local's
|
|
954
|
+
history in one buffer per name, which a copy's ``v[1]`` would share with
|
|
955
|
+
the original's, and refuses the script."""
|
|
956
|
+
fresh: dict[tuple, str] = {}
|
|
957
|
+
|
|
958
|
+
def rename(binding: tuple, name: str) -> str:
|
|
959
|
+
if name == "_" or binding is None:
|
|
960
|
+
return name
|
|
961
|
+
if binding not in fresh:
|
|
962
|
+
candidate, n = f"{name}{suffix}", 0
|
|
963
|
+
while candidate in taken:
|
|
964
|
+
n += 1
|
|
965
|
+
candidate = f"{name}{suffix}_{n}"
|
|
966
|
+
taken.add(candidate)
|
|
967
|
+
fresh[binding] = candidate
|
|
968
|
+
return fresh[binding]
|
|
969
|
+
|
|
970
|
+
for node in _walk(original.body):
|
|
971
|
+
clone = memo[id(node)]
|
|
972
|
+
if isinstance(node, VarDecl) and node.name:
|
|
973
|
+
clone.name = rename(prog.decl_binding.get((id(node), node.name)), node.name)
|
|
974
|
+
elif isinstance(node, TupleAssign):
|
|
975
|
+
clone.names = [rename(prog.decl_binding.get((id(node), name)), name)
|
|
976
|
+
for name in node.names]
|
|
977
|
+
elif isinstance(node, ForStmt):
|
|
978
|
+
clone.var = rename(("bound", id(node), node.var), node.var)
|
|
979
|
+
elif isinstance(node, ForInStmt):
|
|
980
|
+
if node.var:
|
|
981
|
+
clone.var = rename(("bound", id(node), node.var), node.var)
|
|
982
|
+
else:
|
|
983
|
+
clone.vars = [rename(("bound", id(node), name), name)
|
|
984
|
+
for name in node.vars or ()]
|
|
985
|
+
for node in _walk(original.body):
|
|
986
|
+
if isinstance(node, Identifier) and prog.refs.get(id(node)) in fresh:
|
|
987
|
+
memo[id(node)].name = fresh[prog.refs[id(node)]]
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def _payload_params(prog: ScriptIndex, func: str, request: FuncCall) -> list[Identifier]:
|
|
991
|
+
"""The reads of ``func``'s parameters in ``request``'s payload (not a
|
|
992
|
+
tuple's: a helper returning one does not compile)."""
|
|
993
|
+
if _request_name(request) != "security" or isinstance(_request_payload(request),
|
|
994
|
+
TupleLiteral):
|
|
995
|
+
return []
|
|
996
|
+
return [node for node in _walk(_request_payload(request))
|
|
997
|
+
if isinstance(node, Identifier) and prog.refs.get(id(node), ())[:2] == ("param", func)]
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def _analyzer_binds_payload(prog: ScriptIndex, func: str, request: FuncCall,
|
|
1001
|
+
reads: list[Identifier]) -> bool:
|
|
1002
|
+
"""The analyzer's lowering binds these payload reads exactly: each is a
|
|
1003
|
+
``ta.*`` length or a history index, every call of ``func`` is at the top
|
|
1004
|
+
level and passes each parameter one value, the same spelling, which holds
|
|
1005
|
+
no input call (each untitled one has its own key). The analyzer's
|
|
1006
|
+
call-site clones for differing symbols or timeframes give every clone the
|
|
1007
|
+
first call's length. A helper nothing calls never runs; an overloaded
|
|
1008
|
+
one's calls are not told apart, and keep the analyzer's lowering."""
|
|
1009
|
+
calls = prog.calls[func]
|
|
1010
|
+
if not calls or func in prog.overloaded:
|
|
1011
|
+
return True
|
|
1012
|
+
if any(prog.owner.get(id(call)) is not None for call in calls):
|
|
1013
|
+
return False
|
|
1014
|
+
objects, lengths, _ = _payload_positions(prog, request)
|
|
1015
|
+
if any(id(read) not in lengths for read in reads):
|
|
1016
|
+
return False
|
|
1017
|
+
for param in {prog.refs[id(read)][2] for read in reads}:
|
|
1018
|
+
args = [prog.param_arg(func, call, param) for call in calls]
|
|
1019
|
+
if len({context_key(arg) for arg in args}) > 1 or any(
|
|
1020
|
+
id(node) in prog.input_names for arg in args for node in _walk(arg)):
|
|
1021
|
+
return False
|
|
1022
|
+
return True
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def _payload_positions(prog: ScriptIndex, request: FuncCall) -> tuple[set[int], set[int], set[int]]:
|
|
1026
|
+
"""The ids of the payload's history objects (``e`` in ``e[1]``), of its
|
|
1027
|
+
``ta.*`` length arguments and history indices -- where every earlier
|
|
1028
|
+
build lowered a helper parameter without refusing it -- and of the nodes
|
|
1029
|
+
under a builtin call the builder renders on the chart's terms (``nz``,
|
|
1030
|
+
``str.*``, ...) with no ``ta.*`` or user call between them."""
|
|
1031
|
+
from .analyzer.tables import TA_PERIOD_ARG
|
|
1032
|
+
|
|
1033
|
+
objects, lengths, rendered = set(), set(), set()
|
|
1034
|
+
|
|
1035
|
+
def visit(value, under: bool) -> None:
|
|
1036
|
+
if isinstance(value, (list, tuple)):
|
|
1037
|
+
for item in value:
|
|
1038
|
+
visit(item, under)
|
|
1039
|
+
return
|
|
1040
|
+
if isinstance(value, dict):
|
|
1041
|
+
for item in value.values():
|
|
1042
|
+
visit(item, under)
|
|
1043
|
+
return
|
|
1044
|
+
if not isinstance(value, ASTNode):
|
|
1045
|
+
return
|
|
1046
|
+
if under:
|
|
1047
|
+
rendered.add(id(value))
|
|
1048
|
+
if isinstance(value, Subscript):
|
|
1049
|
+
objects.add(id(value.object))
|
|
1050
|
+
lengths.add(id(value.index))
|
|
1051
|
+
elif isinstance(value, FuncCall):
|
|
1052
|
+
callee = value.callee
|
|
1053
|
+
space = (callee.object.name if isinstance(callee, MemberAccess)
|
|
1054
|
+
and isinstance(callee.object, Identifier)
|
|
1055
|
+
and id(callee.object) not in prog.refs else None)
|
|
1056
|
+
if space == "ta":
|
|
1057
|
+
index = TA_PERIOD_ARG.get(callee.member)
|
|
1058
|
+
if index is not None and index < len(value.args):
|
|
1059
|
+
lengths.add(id(value.args[index]))
|
|
1060
|
+
if "length" in value.kwargs:
|
|
1061
|
+
lengths.add(id(value.kwargs["length"]))
|
|
1062
|
+
under = False
|
|
1063
|
+
elif isinstance(callee, Identifier) and callee.name in prog.funcs:
|
|
1064
|
+
under = False
|
|
1065
|
+
elif space != "math":
|
|
1066
|
+
under = True
|
|
1067
|
+
for child in _children(value):
|
|
1068
|
+
visit(child, under)
|
|
1069
|
+
|
|
1070
|
+
visit(_request_payload(request), False)
|
|
1071
|
+
return objects, lengths, rendered
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
# The chart's own series a payload lowers on the requested bars, as a value
|
|
1075
|
+
# and as a history object.
|
|
1076
|
+
_VALUE_SERIES = frozenset({
|
|
1077
|
+
"open", "high", "low", "close", "volume", "hl2", "hlc3", "ohlc4", "hlcc4",
|
|
1078
|
+
})
|
|
1079
|
+
_HISTORY_SERIES = _VALUE_SERIES
|
|
1080
|
+
_VALUE_CALLS = frozenset({"nz", "int", "float"})
|
|
1081
|
+
_CONSTANT_INPUTS = frozenset({"int", "float", "bool", "string"})
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
class _Lowered:
|
|
1085
|
+
"""Whether the request builder lowers a payload parameter's value on the
|
|
1086
|
+
requested bars in the read's place, never on the chart's terms or into
|
|
1087
|
+
C++ that does not compile. It reads a global only when the global is not
|
|
1088
|
+
reassigned or ``var`` and its value is itself lowered. A call's argument
|
|
1089
|
+
names globals declared before the call, which may follow the helper: the
|
|
1090
|
+
copy's read carries ``GLOBAL_ANNOTATION`` (``resolve_payload``), which
|
|
1091
|
+
the builder takes for the binding the analyzer cannot give it there. The
|
|
1092
|
+
analyzer types such a name a float, so a global declared after the
|
|
1093
|
+
helper is put in only when its value is a number or a bool (``_numeric``):
|
|
1094
|
+
a later string input typed the request a double, which did not compile."""
|
|
1095
|
+
|
|
1096
|
+
def __init__(self, prog: ScriptIndex, func: str) -> None:
|
|
1097
|
+
from .analyzer.tables import TA_TUPLE_RETURNS
|
|
1098
|
+
|
|
1099
|
+
self.prog = prog
|
|
1100
|
+
self.tuple_ta = TA_TUPLE_RETURNS
|
|
1101
|
+
# (rule, global name, globals_ok) -> verdict: a global's value is
|
|
1102
|
+
# judged once however many reads share it.
|
|
1103
|
+
self.memo: dict = {}
|
|
1104
|
+
self.declared: dict = {}
|
|
1105
|
+
# The globals declared after the helper.
|
|
1106
|
+
self.later: set[str] = set()
|
|
1107
|
+
helper = prog.program.body.index(prog.funcs[func])
|
|
1108
|
+
for index, stmt in enumerate(prog.program.body):
|
|
1109
|
+
names = ([stmt.name] if isinstance(stmt, VarDecl) and stmt.name
|
|
1110
|
+
else list(stmt.names) if isinstance(stmt, TupleAssign) else [])
|
|
1111
|
+
for name in names:
|
|
1112
|
+
if name in self.declared:
|
|
1113
|
+
continue
|
|
1114
|
+
self.declared[name] = stmt.value if isinstance(stmt, VarDecl) else None
|
|
1115
|
+
if index > helper:
|
|
1116
|
+
self.later.add(name)
|
|
1117
|
+
|
|
1118
|
+
def _global(self, node, seen):
|
|
1119
|
+
"""A stable global's declared value, else None."""
|
|
1120
|
+
if (node.name in seen or self.declared.get(node.name) is None
|
|
1121
|
+
or ("global", node.name) in self.prog.unstable):
|
|
1122
|
+
return None
|
|
1123
|
+
if node.name in self.later and not self._numeric(self.declared[node.name], seen):
|
|
1124
|
+
return None
|
|
1125
|
+
return self.declared[node.name]
|
|
1126
|
+
|
|
1127
|
+
def _numeric(self, node, seen: frozenset) -> bool:
|
|
1128
|
+
"""``node`` is a number or a bool: literals, bar series, ``ta.*`` and
|
|
1129
|
+
``math.*`` calls, numeric inputs and ``input.source``, and operators
|
|
1130
|
+
and ternaries over them, through stable globals."""
|
|
1131
|
+
if isinstance(node, (NumberLiteral, BoolLiteral, NaLiteral)):
|
|
1132
|
+
return True
|
|
1133
|
+
if isinstance(node, Identifier):
|
|
1134
|
+
if node.name in self.prog.program_names:
|
|
1135
|
+
value = self.declared.get(node.name)
|
|
1136
|
+
return (node.name not in seen and value is not None
|
|
1137
|
+
and ("global", node.name) not in self.prog.unstable
|
|
1138
|
+
and self._numeric(value, seen | {node.name}))
|
|
1139
|
+
return node.name in _VALUE_SERIES
|
|
1140
|
+
if isinstance(node, FuncCall):
|
|
1141
|
+
kind = self._call(node)
|
|
1142
|
+
if kind == "input":
|
|
1143
|
+
return node.callee.member in ("int", "float", "bool")
|
|
1144
|
+
if kind == "value":
|
|
1145
|
+
return bool(node.args) and all(self._numeric(a, seen) for a in node.args)
|
|
1146
|
+
if (kind is None and isinstance(node.callee, Identifier)
|
|
1147
|
+
and id(node.callee) not in self.prog.refs
|
|
1148
|
+
and self._inlined_function(node.callee.name)):
|
|
1149
|
+
return self._inlined_numeric(node.callee.name)
|
|
1150
|
+
return kind in ("ta", "math", "source", "stop")
|
|
1151
|
+
if isinstance(node, BinOp):
|
|
1152
|
+
return self._numeric(node.left, seen) and self._numeric(node.right, seen)
|
|
1153
|
+
if isinstance(node, UnaryOp):
|
|
1154
|
+
return self._numeric(node.operand, seen)
|
|
1155
|
+
if isinstance(node, Ternary):
|
|
1156
|
+
return self._numeric(node.true_val, seen) and self._numeric(node.false_val, seen)
|
|
1157
|
+
if isinstance(node, Subscript):
|
|
1158
|
+
return self._numeric(node.object, seen)
|
|
1159
|
+
return False
|
|
1160
|
+
|
|
1161
|
+
def _call(self, node) -> str | None:
|
|
1162
|
+
"""``ta``, ``math``, ``input``, ``source`` or ``value`` for a call the
|
|
1163
|
+
builder lowers, else None."""
|
|
1164
|
+
callee = node.callee
|
|
1165
|
+
if (isinstance(callee, MemberAccess) and isinstance(callee.object, Identifier)
|
|
1166
|
+
and callee.object.name not in self.declared
|
|
1167
|
+
and id(callee.object) not in self.prog.refs):
|
|
1168
|
+
space, member = callee.object.name, callee.member
|
|
1169
|
+
if space == "ta" and member not in self.tuple_ta:
|
|
1170
|
+
return "ta"
|
|
1171
|
+
if space == "math":
|
|
1172
|
+
return "math"
|
|
1173
|
+
if space == "input" and member in _CONSTANT_INPUTS:
|
|
1174
|
+
return "input"
|
|
1175
|
+
if space == "input" and member == "source":
|
|
1176
|
+
# The series it selects, read on the requested bars.
|
|
1177
|
+
return "source"
|
|
1178
|
+
if space == "request" and member == "security" and self._heikinashi_request(node):
|
|
1179
|
+
# A request of Heikin-Ashi bars inside the payload: evaluating
|
|
1180
|
+
# it stops the run (the codegen's
|
|
1181
|
+
# ``_security_nested_heikinashi_request``), so a selection
|
|
1182
|
+
# that never takes it reads the requested bars.
|
|
1183
|
+
return "stop"
|
|
1184
|
+
return None
|
|
1185
|
+
if (isinstance(callee, Identifier) and callee.name == "input"
|
|
1186
|
+
and callee.name not in self.declared and callee.name not in self.prog.funcs
|
|
1187
|
+
and node.args and isinstance(node.args[0], Identifier)
|
|
1188
|
+
and node.args[0].name in _VALUE_SERIES
|
|
1189
|
+
and id(node.args[0]) not in self.prog.refs):
|
|
1190
|
+
# ``input(close)``, the source overload.
|
|
1191
|
+
return "source"
|
|
1192
|
+
if (isinstance(callee, Identifier) and callee.name in _VALUE_CALLS
|
|
1193
|
+
and callee.name not in self.declared and callee.name not in self.prog.funcs):
|
|
1194
|
+
return "value"
|
|
1195
|
+
return None
|
|
1196
|
+
|
|
1197
|
+
def _judged(self, rule: str, node, seen, globals_ok, judge) -> bool:
|
|
1198
|
+
key = (rule, node.name, globals_ok)
|
|
1199
|
+
if key not in self.memo:
|
|
1200
|
+
declared = self._global(node, seen)
|
|
1201
|
+
self.memo[key] = declared is not None and judge(declared, seen | {node.name})
|
|
1202
|
+
return self.memo[key]
|
|
1203
|
+
|
|
1204
|
+
def _heikinashi_request(self, node) -> bool:
|
|
1205
|
+
"""``node`` (a ``request.security``) requests Heikin-Ashi bars: a
|
|
1206
|
+
``ticker.heikinashi(...)`` symbol, written there or held by a
|
|
1207
|
+
global."""
|
|
1208
|
+
symbol = node.args[0] if node.args else node.kwargs.get("symbol")
|
|
1209
|
+
if isinstance(symbol, Identifier) and self.prog.refs.get(id(symbol), ("",))[0] == "global":
|
|
1210
|
+
symbol = self.declared.get(symbol.name)
|
|
1211
|
+
return (isinstance(symbol, FuncCall) and isinstance(symbol.callee, MemberAccess)
|
|
1212
|
+
and isinstance(symbol.callee.object, Identifier)
|
|
1213
|
+
and symbol.callee.object.name == "ticker"
|
|
1214
|
+
and id(symbol.callee.object) not in self.prog.refs
|
|
1215
|
+
and symbol.callee.member == "heikinashi")
|
|
1216
|
+
|
|
1217
|
+
def _user_call(self, node, seen: frozenset, globals_ok: bool) -> bool:
|
|
1218
|
+
"""``node``, a global's value, is a call of a user function the
|
|
1219
|
+
builder inlines whole on the requested bar (``_inlined_function``)
|
|
1220
|
+
on lowered arguments. The codegen lowers such a global once per
|
|
1221
|
+
evaluator, and keeps its history on the requested clock
|
|
1222
|
+
(``_security_global_history_value``)."""
|
|
1223
|
+
return (isinstance(node, FuncCall) and isinstance(node.callee, Identifier)
|
|
1224
|
+
and id(node.callee) not in self.prog.refs
|
|
1225
|
+
and self._inlined_function(node.callee.name)
|
|
1226
|
+
and all(self.value(a, seen, globals_ok)
|
|
1227
|
+
for a in (*node.args, *node.kwargs.values())))
|
|
1228
|
+
|
|
1229
|
+
def _inlined_function(self, name: str, seen: frozenset = frozenset()) -> bool:
|
|
1230
|
+
"""``name`` is a user function the builder inlines whole on the
|
|
1231
|
+
requested bar: one definition whose body declares plain locals and
|
|
1232
|
+
ends in an expression, reading its parameters, its locals, bar series
|
|
1233
|
+
and literals through operators, ternaries, single-valued ``ta.*``,
|
|
1234
|
+
``math.*``, ``int`` / ``float`` / ``nz`` and calls of such functions.
|
|
1235
|
+
No ``var``, reassignment, branch or loop statement, history read,
|
|
1236
|
+
global, request or order."""
|
|
1237
|
+
if name in seen or name in self.prog.overloaded or name not in self.prog.funcs:
|
|
1238
|
+
return False
|
|
1239
|
+
key = ("function", name)
|
|
1240
|
+
if key not in self.memo:
|
|
1241
|
+
self.memo[key] = False # a recursive helper is not one
|
|
1242
|
+
body = self.prog.funcs[name].body
|
|
1243
|
+
ok = bool(body)
|
|
1244
|
+
for index, stmt in enumerate(body):
|
|
1245
|
+
last = index == len(body) - 1
|
|
1246
|
+
if isinstance(stmt, VarDecl) and stmt.name:
|
|
1247
|
+
ok = (not (stmt.is_var or stmt.is_varip)
|
|
1248
|
+
and ("local", id(stmt)) not in self.prog.unstable)
|
|
1249
|
+
expr = stmt.value
|
|
1250
|
+
elif last:
|
|
1251
|
+
expr = stmt.expr if isinstance(stmt, ExprStmt) else stmt
|
|
1252
|
+
else:
|
|
1253
|
+
ok = False
|
|
1254
|
+
if not ok or not self._inlined_expr(name, expr, seen | {name}):
|
|
1255
|
+
ok = False
|
|
1256
|
+
break
|
|
1257
|
+
self.memo[key] = ok
|
|
1258
|
+
return self.memo[key]
|
|
1259
|
+
|
|
1260
|
+
def _inlined_numeric(self, name: str, node=None, seen: frozenset = frozenset()) -> bool:
|
|
1261
|
+
"""The result of ``_inlined_function`` ``name`` (or ``node`` in its
|
|
1262
|
+
body) is a number or a bool: numeric literals, bar series, ``ta.*``,
|
|
1263
|
+
``math.*``, ``int`` / ``float``, comparisons and logic, arithmetic
|
|
1264
|
+
and ternaries over them, its numeric locals and such calls. A
|
|
1265
|
+
parameter or a string is not."""
|
|
1266
|
+
if node is None:
|
|
1267
|
+
if name in seen:
|
|
1268
|
+
return False
|
|
1269
|
+
last = self.prog.funcs[name].body[-1]
|
|
1270
|
+
node = (last.value if isinstance(last, VarDecl)
|
|
1271
|
+
else last.expr if isinstance(last, ExprStmt) else last)
|
|
1272
|
+
seen = seen | {name}
|
|
1273
|
+
if isinstance(node, (NumberLiteral, BoolLiteral, NaLiteral)):
|
|
1274
|
+
return True
|
|
1275
|
+
if isinstance(node, Identifier):
|
|
1276
|
+
ref = self.prog.refs.get(id(node))
|
|
1277
|
+
if ref is None:
|
|
1278
|
+
return node.name in _VALUE_SERIES
|
|
1279
|
+
decl = self.prog.decls.get(ref) if ref[0] == "local" else None
|
|
1280
|
+
return decl is not None and self._inlined_numeric(name, decl.value, seen)
|
|
1281
|
+
if isinstance(node, BinOp):
|
|
1282
|
+
if node.op in ("==", "!=", "<", ">", "<=", ">=", "and", "or"):
|
|
1283
|
+
return True
|
|
1284
|
+
return (self._inlined_numeric(name, node.left, seen)
|
|
1285
|
+
and self._inlined_numeric(name, node.right, seen))
|
|
1286
|
+
if isinstance(node, UnaryOp):
|
|
1287
|
+
return node.op == "not" or self._inlined_numeric(name, node.operand, seen)
|
|
1288
|
+
if isinstance(node, Ternary):
|
|
1289
|
+
return (self._inlined_numeric(name, node.true_val, seen)
|
|
1290
|
+
and self._inlined_numeric(name, node.false_val, seen))
|
|
1291
|
+
if isinstance(node, FuncCall):
|
|
1292
|
+
callee = node.callee
|
|
1293
|
+
if isinstance(callee, MemberAccess):
|
|
1294
|
+
return True # ``_inlined_expr`` admits ``ta.*`` and ``math.*`` only
|
|
1295
|
+
if callee.name in ("int", "float"):
|
|
1296
|
+
return True
|
|
1297
|
+
if callee.name == "nz":
|
|
1298
|
+
return bool(node.args) and self._inlined_numeric(name, node.args[0], seen)
|
|
1299
|
+
return self._inlined_numeric(callee.name, None, seen)
|
|
1300
|
+
return False
|
|
1301
|
+
|
|
1302
|
+
def _inlined_expr(self, func: str, node, seen: frozenset) -> bool:
|
|
1303
|
+
if isinstance(node, _LITERALS):
|
|
1304
|
+
return True
|
|
1305
|
+
if isinstance(node, Identifier):
|
|
1306
|
+
ref = self.prog.refs.get(id(node))
|
|
1307
|
+
if ref is None:
|
|
1308
|
+
return node.name in _VALUE_SERIES
|
|
1309
|
+
if ref[0] == "param":
|
|
1310
|
+
return ref[1] == func
|
|
1311
|
+
return ref[0] == "local" and ref not in self.prog.unstable
|
|
1312
|
+
if isinstance(node, BinOp):
|
|
1313
|
+
return (self._inlined_expr(func, node.left, seen)
|
|
1314
|
+
and self._inlined_expr(func, node.right, seen))
|
|
1315
|
+
if isinstance(node, UnaryOp):
|
|
1316
|
+
return self._inlined_expr(func, node.operand, seen)
|
|
1317
|
+
if isinstance(node, Ternary):
|
|
1318
|
+
return all(self._inlined_expr(func, part, seen)
|
|
1319
|
+
for part in (node.condition, node.true_val, node.false_val))
|
|
1320
|
+
if isinstance(node, FuncCall):
|
|
1321
|
+
args = (*node.args, *node.kwargs.values())
|
|
1322
|
+
callee = node.callee
|
|
1323
|
+
if isinstance(callee, Identifier) and id(callee) not in self.prog.refs:
|
|
1324
|
+
known = (callee.name in _VALUE_CALLS and callee.name not in self.prog.funcs
|
|
1325
|
+
or self._inlined_function(callee.name, seen))
|
|
1326
|
+
elif (isinstance(callee, MemberAccess) and isinstance(callee.object, Identifier)
|
|
1327
|
+
and id(callee.object) not in self.prog.refs):
|
|
1328
|
+
known = (callee.object.name == "math"
|
|
1329
|
+
or callee.object.name == "ta" and callee.member not in self.tuple_ta)
|
|
1330
|
+
else:
|
|
1331
|
+
known = False
|
|
1332
|
+
return known and all(self._inlined_expr(func, a, seen) for a in args)
|
|
1333
|
+
return False
|
|
1334
|
+
|
|
1335
|
+
def value(self, node, seen: frozenset = frozenset(), globals_ok: bool = True) -> bool:
|
|
1336
|
+
if isinstance(node, _LITERALS):
|
|
1337
|
+
return True
|
|
1338
|
+
if isinstance(node, Identifier):
|
|
1339
|
+
if node.name in self.prog.program_names:
|
|
1340
|
+
return globals_ok and self._judged(
|
|
1341
|
+
"value", node, seen, globals_ok,
|
|
1342
|
+
lambda declared, inner: self.value(declared, inner, globals_ok)
|
|
1343
|
+
or self._user_call(declared, inner, globals_ok))
|
|
1344
|
+
return node.name in _VALUE_SERIES
|
|
1345
|
+
if isinstance(node, FuncCall):
|
|
1346
|
+
# An input's arguments are constants (TradingView takes const
|
|
1347
|
+
# ones): its value is the same on every bar. ``nz`` and the casts
|
|
1348
|
+
# render on the chart's terms: no global below them.
|
|
1349
|
+
kind = self._call(node)
|
|
1350
|
+
inner_ok = globals_ok and kind != "value"
|
|
1351
|
+
return kind in ("input", "source", "stop") or (kind is not None and all(
|
|
1352
|
+
self.value(a, seen, inner_ok) for a in (*node.args, *node.kwargs.values())))
|
|
1353
|
+
if isinstance(node, BinOp):
|
|
1354
|
+
return self.value(node.left, seen, globals_ok) and self.value(
|
|
1355
|
+
node.right, seen, globals_ok)
|
|
1356
|
+
if isinstance(node, UnaryOp):
|
|
1357
|
+
return self.value(node.operand, seen, globals_ok)
|
|
1358
|
+
if isinstance(node, Ternary):
|
|
1359
|
+
return all(self.value(part, seen, globals_ok)
|
|
1360
|
+
for part in (node.condition, node.true_val, node.false_val))
|
|
1361
|
+
if isinstance(node, Subscript):
|
|
1362
|
+
return (self.history(node.object, seen, globals_ok)
|
|
1363
|
+
and self.length(node.index, seen))
|
|
1364
|
+
return False
|
|
1365
|
+
|
|
1366
|
+
def history(self, node, seen: frozenset = frozenset(), globals_ok: bool = True) -> bool:
|
|
1367
|
+
"""``node`` is a history object: a series with requested-bar history,
|
|
1368
|
+
a ``ta.*`` call (inline, or a global's value), an ``input.source``
|
|
1369
|
+
(the series it selects), an inline operator expression. Not a
|
|
1370
|
+
literal, ``na``, another history read, or a global holding an
|
|
1371
|
+
operator expression."""
|
|
1372
|
+
if isinstance(node, Identifier):
|
|
1373
|
+
if node.name in self.prog.program_names:
|
|
1374
|
+
# A global holding a user call or an operator expression
|
|
1375
|
+
# keeps its history on the requested clock
|
|
1376
|
+
# (``_security_global_history_value``).
|
|
1377
|
+
return globals_ok and self._judged(
|
|
1378
|
+
"history", node, seen, globals_ok,
|
|
1379
|
+
lambda declared, inner: (
|
|
1380
|
+
isinstance(declared, (Identifier, FuncCall))
|
|
1381
|
+
and self.history(declared, inner, globals_ok))
|
|
1382
|
+
or self._user_call(declared, inner, globals_ok)
|
|
1383
|
+
or (isinstance(declared, (BinOp, UnaryOp, Ternary))
|
|
1384
|
+
and self._numeric(declared, inner)
|
|
1385
|
+
and self.value(declared, inner, globals_ok)))
|
|
1386
|
+
return node.name in _HISTORY_SERIES
|
|
1387
|
+
if isinstance(node, FuncCall):
|
|
1388
|
+
kind = self._call(node)
|
|
1389
|
+
return kind == "source" or (kind == "ta" and self.value(node, seen, globals_ok))
|
|
1390
|
+
if isinstance(node, (BinOp, UnaryOp, Ternary)):
|
|
1391
|
+
return self.value(node, seen, globals_ok)
|
|
1392
|
+
return False
|
|
1393
|
+
|
|
1394
|
+
def length(self, node, seen: frozenset = frozenset()) -> bool:
|
|
1395
|
+
"""``node`` is the same on every bar: literals, constant inputs,
|
|
1396
|
+
``math.*`` and operators over them, globals holding them."""
|
|
1397
|
+
if isinstance(node, _LITERALS):
|
|
1398
|
+
return True
|
|
1399
|
+
if isinstance(node, Identifier):
|
|
1400
|
+
return node.name in self.prog.program_names and self._judged(
|
|
1401
|
+
"length", node, seen, True, self.length)
|
|
1402
|
+
if isinstance(node, FuncCall):
|
|
1403
|
+
kind = self._call(node)
|
|
1404
|
+
return kind == "input" or (kind in ("math", "value") and all(
|
|
1405
|
+
self.length(a, seen) for a in (*node.args, *node.kwargs.values())))
|
|
1406
|
+
if isinstance(node, BinOp):
|
|
1407
|
+
return self.length(node.left, seen) and self.length(node.right, seen)
|
|
1408
|
+
if isinstance(node, UnaryOp):
|
|
1409
|
+
return self.length(node.operand, seen)
|
|
1410
|
+
if isinstance(node, Ternary):
|
|
1411
|
+
return all(self.length(part, seen)
|
|
1412
|
+
for part in (node.condition, node.true_val, node.false_val))
|
|
1413
|
+
return False
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
def _payload_values(prog: ScriptIndex, func: str, request: FuncCall, reads: list[Identifier],
|
|
1417
|
+
env: dict) -> tuple:
|
|
1418
|
+
"""Each parameter ``reads`` reads, with its value on this call path, when
|
|
1419
|
+
the builder lowers that value on the requested bars in the place of every
|
|
1420
|
+
read of it (``_Lowered``): a history object, a ``ta.*`` length or history
|
|
1421
|
+
index, a value -- naming no global under a builtin rendered on the
|
|
1422
|
+
chart's terms (``nz(g)`` would read the chart's ``g``)."""
|
|
1423
|
+
objects, lengths, rendered = _payload_positions(prog, request)
|
|
1424
|
+
lowered = _Lowered(prog, func)
|
|
1425
|
+
fdef = prog.funcs[func]
|
|
1426
|
+
# A name the helper declares would capture a global of the same name.
|
|
1427
|
+
shadowing = set(fdef.params)
|
|
1428
|
+
for node in _walk(fdef.body):
|
|
1429
|
+
if isinstance(node, VarDecl):
|
|
1430
|
+
shadowing.add(node.name)
|
|
1431
|
+
elif isinstance(node, TupleAssign):
|
|
1432
|
+
shadowing.update(node.names)
|
|
1433
|
+
elif isinstance(node, ForStmt):
|
|
1434
|
+
shadowing.add(node.var)
|
|
1435
|
+
elif isinstance(node, ForInStmt):
|
|
1436
|
+
shadowing.update([node.var] if node.var else list(node.vars or ()))
|
|
1437
|
+
values = []
|
|
1438
|
+
for param in sorted({prog.refs[id(read)][2] for read in reads}):
|
|
1439
|
+
arg, caller_env = env.get(param, (None, None))
|
|
1440
|
+
if arg is None:
|
|
1441
|
+
raise _Unresolvable(f"parameter '{param}' has no argument")
|
|
1442
|
+
value = prog.resolve_payload(arg, caller_env)
|
|
1443
|
+
for read in (r for r in reads if prog.refs[id(r)][2] == param):
|
|
1444
|
+
if id(read) in objects:
|
|
1445
|
+
ok = lowered.history(value, globals_ok=id(read) not in rendered)
|
|
1446
|
+
elif id(read) in lengths:
|
|
1447
|
+
ok = lowered.length(value)
|
|
1448
|
+
else:
|
|
1449
|
+
ok = lowered.value(value, globals_ok=id(read) not in rendered)
|
|
1450
|
+
if not ok:
|
|
1451
|
+
raise _Unresolvable(f"the value of '{param}' is not lowered on the requested bars")
|
|
1452
|
+
names = {node.name for node in _walk(value) if isinstance(node, Identifier)}
|
|
1453
|
+
if names & shadowing:
|
|
1454
|
+
raise _Unresolvable(f"'{func}' declares '{sorted(names & shadowing)[0]}'")
|
|
1455
|
+
values.append((param, value))
|
|
1456
|
+
return tuple(values)
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
def _alias_value(prog: ScriptIndex, value):
|
|
1460
|
+
"""``value`` with each global alias replaced by its declared value, so a
|
|
1461
|
+
Heikin-Ashi symbol declared after the helper reads as one."""
|
|
1462
|
+
seen: set[str] = set()
|
|
1463
|
+
while (isinstance(value, Identifier) and value.name not in seen
|
|
1464
|
+
and prog.refs.get(id(value), ("global",))[0] == "global"):
|
|
1465
|
+
decl = prog.decls.get(("global", value.name))
|
|
1466
|
+
if decl is None or decl.value is None or ("global", value.name) in prog.unstable:
|
|
1467
|
+
break
|
|
1468
|
+
seen.add(value.name)
|
|
1469
|
+
value = decl.value
|
|
1470
|
+
return value
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def _reached(prog: ScriptIndex) -> set[str]:
|
|
1474
|
+
"""The helpers a top-level statement, or a method, calls, transitively."""
|
|
1475
|
+
out: set[str] = set()
|
|
1476
|
+
changed = True
|
|
1477
|
+
while changed:
|
|
1478
|
+
changed = False
|
|
1479
|
+
for name, calls in prog.calls.items():
|
|
1480
|
+
if name not in out and any(
|
|
1481
|
+
(owner := prog.owner.get(id(call))) is None or owner in out
|
|
1482
|
+
or owner not in prog.funcs for call in calls):
|
|
1483
|
+
out.add(name)
|
|
1484
|
+
changed = True
|
|
1485
|
+
return out
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
def _mark_unreached(prog: ScriptIndex, reached: set[str]) -> None:
|
|
1489
|
+
"""Annotate each payload read of a parameter of a helper nothing reaches:
|
|
1490
|
+
it never runs, so it does not warn."""
|
|
1491
|
+
for owner, requests in prog.requests.items():
|
|
1492
|
+
if owner in prog.funcs and owner not in reached:
|
|
1493
|
+
for request in requests:
|
|
1494
|
+
for read in _payload_params(prog, owner, request):
|
|
1495
|
+
read.annotations = {**(read.annotations or {}),
|
|
1496
|
+
UNREACHED_ANNOTATION: True}
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
def _type_context_params(prog: ScriptIndex, owned: dict, leads: set[str]) -> None:
|
|
1500
|
+
"""Declare ``string`` every untyped helper parameter that carries a
|
|
1501
|
+
request's symbol or timeframe as it is -- the request's own argument, or
|
|
1502
|
+
passed on whole to such a parameter of another helper. TradingView
|
|
1503
|
+
requires a string there; a helper reached only through another helper's
|
|
1504
|
+
parameter otherwise defaults the parameter to ``double`` while its
|
|
1505
|
+
caller passes a string, which does not compile."""
|
|
1506
|
+
carriers: dict[str, set[str]] = {}
|
|
1507
|
+
|
|
1508
|
+
def bare_param(arg, func: str) -> str | None:
|
|
1509
|
+
binding = prog.refs.get(id(arg)) if isinstance(arg, Identifier) else None
|
|
1510
|
+
if binding is not None and binding[0] == "param" and binding[1] == func:
|
|
1511
|
+
return binding[2]
|
|
1512
|
+
return None
|
|
1513
|
+
|
|
1514
|
+
for func, requests in owned.items():
|
|
1515
|
+
for request in requests:
|
|
1516
|
+
for arg in _request_args(request):
|
|
1517
|
+
param = bare_param(arg, func)
|
|
1518
|
+
if param is not None:
|
|
1519
|
+
carriers.setdefault(func, set()).add(param)
|
|
1520
|
+
changed = True
|
|
1521
|
+
while changed:
|
|
1522
|
+
changed = False
|
|
1523
|
+
for func in leads:
|
|
1524
|
+
for call in _body_calls(prog, func, leads):
|
|
1525
|
+
callee = call.callee.name
|
|
1526
|
+
for param in carriers.get(callee, ()):
|
|
1527
|
+
passed = bare_param(prog.param_arg(callee, call, param), func)
|
|
1528
|
+
if passed is not None and passed not in carriers.get(func, set()):
|
|
1529
|
+
carriers.setdefault(func, set()).add(passed)
|
|
1530
|
+
changed = True
|
|
1531
|
+
for func, params in carriers.items():
|
|
1532
|
+
fdef = prog.funcs[func]
|
|
1533
|
+
notes = fdef.annotations = dict(fdef.annotations or {})
|
|
1534
|
+
hints = list(notes.get("param_type_hints") or [])
|
|
1535
|
+
hints += [None] * (len(fdef.params) - len(hints))
|
|
1536
|
+
for index, param in enumerate(fdef.params):
|
|
1537
|
+
if param in params and not hints[index]:
|
|
1538
|
+
hints[index] = "string"
|
|
1539
|
+
notes["param_type_hints"] = hints
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
def _reachable(prog: ScriptIndex, func: str, leads: set[str]) -> set[str]:
|
|
1543
|
+
"""The helpers ``func``'s body calls, transitively."""
|
|
1544
|
+
seen: set[str] = set()
|
|
1545
|
+
stack = [func]
|
|
1546
|
+
while stack:
|
|
1547
|
+
name = stack.pop()
|
|
1548
|
+
for call in _body_calls(prog, name, leads):
|
|
1549
|
+
callee = call.callee.name
|
|
1550
|
+
if callee not in seen:
|
|
1551
|
+
seen.add(callee)
|
|
1552
|
+
stack.append(callee)
|
|
1553
|
+
return seen
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
def _body_calls(prog: ScriptIndex, func: str, leads: set[str]) -> list[FuncCall]:
|
|
1557
|
+
"""The calls in ``func``'s body to helpers leading to owned requests, in
|
|
1558
|
+
source order."""
|
|
1559
|
+
return [node for node in _walk(prog.funcs[func].body)
|
|
1560
|
+
if isinstance(node, FuncCall) and isinstance(node.callee, Identifier)
|
|
1561
|
+
and node.callee.name in leads and prog.owner.get(id(node)) == func
|
|
1562
|
+
and id(node) in prog.call_ids]
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
def _analyzer_resolves(prog: ScriptIndex, func: str, symbol, tf) -> bool:
|
|
1566
|
+
"""The analyzer's call-site clones serve this request: its symbol and
|
|
1567
|
+
timeframe are each a global expression or a bare parameter of ``func``,
|
|
1568
|
+
and every call of ``func`` passes a global expression for them."""
|
|
1569
|
+
params = []
|
|
1570
|
+
for arg in (symbol, tf):
|
|
1571
|
+
if arg is None or not prog.depends_on_scope(arg):
|
|
1572
|
+
continue
|
|
1573
|
+
binding = prog.refs.get(id(arg)) if isinstance(arg, Identifier) else None
|
|
1574
|
+
if binding is None or binding[0] != "param" or binding[1] != func:
|
|
1575
|
+
return False
|
|
1576
|
+
params.append(binding[2])
|
|
1577
|
+
fdef = prog.funcs[func]
|
|
1578
|
+
for call in prog.calls[func]:
|
|
1579
|
+
for param in params:
|
|
1580
|
+
index = fdef.params.index(param)
|
|
1581
|
+
if param in call.kwargs or index >= len(call.args):
|
|
1582
|
+
return False
|
|
1583
|
+
if prog.depends_on_scope(call.args[index]):
|
|
1584
|
+
return False
|
|
1585
|
+
return True
|