@pineforge/codegen-pyodide 1.0.0 → 1.1.0
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/glue.py +2 -1
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +24 -9
- package/pineforge_codegen/analyzer/base.py +190 -1
- package/pineforge_codegen/analyzer/call_handlers.py +2 -0
- package/pineforge_codegen/analyzer/contracts.py +6 -0
- package/pineforge_codegen/analyzer/types.py +16 -5
- package/pineforge_codegen/ast_nodes.py +7 -0
- package/pineforge_codegen/codegen/base.py +136 -14
- package/pineforge_codegen/codegen/checked_settings.py +272 -0
- package/pineforge_codegen/codegen/collection_history.py +200 -0
- package/pineforge_codegen/codegen/emit_top.py +76 -14
- package/pineforge_codegen/codegen/helpers.py +12 -2
- package/pineforge_codegen/codegen/host_members.py +8 -0
- package/pineforge_codegen/codegen/input.py +36 -3
- package/pineforge_codegen/codegen/security.py +56 -1
- package/pineforge_codegen/codegen/ta.py +65 -1
- package/pineforge_codegen/codegen/types.py +136 -8
- package/pineforge_codegen/codegen/visit_call.py +92 -3
- package/pineforge_codegen/codegen/visit_expr.py +202 -3
- package/pineforge_codegen/codegen/visit_stmt.py +19 -4
- package/pineforge_codegen/collection_history.py +1595 -0
- package/pineforge_codegen/external_requests.py +3 -0
- package/pineforge_codegen/parser.py +19 -4
- package/pineforge_codegen/request_discovery.py +506 -0
- package/pineforge_codegen/support_checker.py +37 -0
- package/pineforge_codegen-1.1.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/transpile.worker.mjs +2 -1
- package/pineforge_codegen-1.0.0.tar.gz +0 -0
|
@@ -0,0 +1,1595 @@
|
|
|
1
|
+
"""The history of an array or a matrix variable (``a[k]``, ``m[k]``).
|
|
2
|
+
|
|
3
|
+
TradingView (``tests/fixtures/array_history_tv``) reads ``a[k]`` as a copy of
|
|
4
|
+
the collection as the variable left it at the end of its scope's execution k
|
|
5
|
+
executions back -- the bar k bars back, for a variable of the script's top
|
|
6
|
+
level -- not as the array the variable holds now: a ``var`` array's history
|
|
7
|
+
is one element shorter than the array it keeps growing. The copy is
|
|
8
|
+
read-only: a change to it, to a slice of it or through a variable or a
|
|
9
|
+
parameter bound to it stops the run (RE10051), and a method on it before the
|
|
10
|
+
variable has a history stops the run too (RE10052 for an array, RE10053 for a
|
|
11
|
+
matrix). A ``for...in`` loop over ``a[k]`` iterates the array the variable
|
|
12
|
+
holds now.
|
|
13
|
+
|
|
14
|
+
The codegen keeps such a variable's own ``std::vector`` or matrix, so every
|
|
15
|
+
operation on the variable keeps its C++, and beside it the copies its
|
|
16
|
+
executions left (``COLLECTION_HISTORY_CPP``): each execution of the
|
|
17
|
+
declaration -- each bar, for a ``var`` -- opens a slot, and the end of the bar
|
|
18
|
+
(or the next declaration of the same name, in a sibling block) closes it with
|
|
19
|
+
a copy of the value. ``a[1]`` used to lower to the current array's element 1
|
|
20
|
+
and ``m[1]`` to a ``Series<double>``; neither compiled where the script reads
|
|
21
|
+
an array or a matrix.
|
|
22
|
+
|
|
23
|
+
``CollectionHistoryChecker.decide`` gives every read one of three outcomes:
|
|
24
|
+
|
|
25
|
+
* ``SUPPORTED``: a variable of the script's top level or of one of its blocks,
|
|
26
|
+
read where a built-in reads or changes it, by ``na()``, as a ``for...in``
|
|
27
|
+
iterable, or bound to a variable or given to a function that only reads it.
|
|
28
|
+
The analyzer annotates the read (``COLLECTION_HISTORY_ANNOTATION``).
|
|
29
|
+
* ``LEGACY``: every other read that TradingView accepts and whose earlier
|
|
30
|
+
lowering compiled (the element ``a[k]`` of the current array, a parameter's
|
|
31
|
+
current collection) keeps that lowering, and its warning: ``na()`` and
|
|
32
|
+
``str.tostring()`` in a function, a request payload or a loop, a
|
|
33
|
+
parameter's history, a dead function's body.
|
|
34
|
+
* ``REFUSED``: what TradingView refuses (CE10123, CE10173, CE10101, CE10122,
|
|
35
|
+
CE10009, CE10013), and the TradingView-valid uses whose earlier lowering
|
|
36
|
+
could not compile (it read an element where the script needs the
|
|
37
|
+
collection, or the current collection where it needs a value): a change
|
|
38
|
+
through a variable or a slice bound to the history, a matrix's history in
|
|
39
|
+
a function, a selection's or a function's result that needs the collection.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
from dataclasses import dataclass, field
|
|
45
|
+
|
|
46
|
+
from .ast_nodes import (
|
|
47
|
+
Assignment, BinOp, ExprStmt, ForInStmt, ForStmt, FuncCall, FuncDef,
|
|
48
|
+
Identifier, IfStmt, MemberAccess, MethodDef, NumberLiteral, Program,
|
|
49
|
+
Subscript, SwitchStmt, Ternary, TupleAssign, TupleLiteral, UnaryOp,
|
|
50
|
+
VarDecl, WhileStmt,
|
|
51
|
+
)
|
|
52
|
+
from .limits import iter_ast_nodes, syntax_children
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Set on a ``Subscript`` the checker supports: a dict with the variable
|
|
56
|
+
# (``var``), its history member (``member``), its kind (``array`` /
|
|
57
|
+
# ``matrix``) and TypeSpec (``spec``), and how the value is used (``use``):
|
|
58
|
+
# ``read`` (a built-in reads it), ``change`` (a built-in changes it: the run
|
|
59
|
+
# stops), ``copy`` (bound to a variable or given to a user function, which
|
|
60
|
+
# read it), ``loop`` (a for...in iterable: the variable's current array, as
|
|
61
|
+
# TradingView iterates) or ``na`` (``na()``).
|
|
62
|
+
COLLECTION_HISTORY_ANNOTATION = "pf_collection_history"
|
|
63
|
+
|
|
64
|
+
SUPPORTED = "supported"
|
|
65
|
+
LEGACY = "legacy"
|
|
66
|
+
REFUSED = "refused"
|
|
67
|
+
|
|
68
|
+
# TradingView's runtime answers (fixtures/array_history_tv README).
|
|
69
|
+
HISTORICAL_CHANGE_MESSAGE = (
|
|
70
|
+
"Cannot modify the elements of a historical array or any slices of that "
|
|
71
|
+
"array. Instead of modifying an array referenced by an ID retrieved with "
|
|
72
|
+
"the `[]` operator, create a shallow copy of the array with "
|
|
73
|
+
"`array.copy()`, then modify the copy or a slice of that copy."
|
|
74
|
+
)
|
|
75
|
+
NA_ARRAY_MESSAGE = "Cannot call array methods when id of array is na."
|
|
76
|
+
NA_MATRIX_MESSAGE = "Cannot call matrix methods when id of matrix is na."
|
|
77
|
+
|
|
78
|
+
# The built-ins that change the array or matrix they are called on (method
|
|
79
|
+
# syntax) or given first (namespace syntax).
|
|
80
|
+
ARRAY_CHANGING_METHODS = frozenset({
|
|
81
|
+
"set", "push", "unshift", "insert", "pop", "shift", "remove", "clear",
|
|
82
|
+
"fill", "sort", "reverse", "concat",
|
|
83
|
+
})
|
|
84
|
+
MATRIX_CHANGING_METHODS = frozenset({
|
|
85
|
+
"set", "fill", "add_row", "add_col", "remove_row", "remove_col",
|
|
86
|
+
"swap_rows", "swap_columns", "reshape", "reverse", "sort", "concat",
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
# The parameters (namespace positions) of the array and matrix built-ins that
|
|
90
|
+
# take a collection besides the first: every other one takes a value, where
|
|
91
|
+
# TradingView refuses an array (CE10123: fixtures/array_history_tv README).
|
|
92
|
+
_COLLECTION_SLOTS = {
|
|
93
|
+
"array": {"concat": {1}, "covariance": {1}},
|
|
94
|
+
"matrix": {"concat": {1}, "sum": {1}, "diff": {1}, "mult": {1},
|
|
95
|
+
"kron": {1}, "add_row": {2}, "add_col": {2}},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Type hints TradingView refuses an array or a matrix for (CE10173).
|
|
99
|
+
_SCALAR_HINTS = frozenset({"float", "int", "bool", "string", "color"})
|
|
100
|
+
|
|
101
|
+
# Built-in namespaces whose functions share a name with an array method
|
|
102
|
+
# (``math.abs``, ``str.join``...): a call of theirs is no collection method.
|
|
103
|
+
_BUILTIN_NAMESPACES = frozenset({
|
|
104
|
+
"math", "str", "ta", "strategy", "request", "input", "color", "timeframe",
|
|
105
|
+
"syminfo", "barstate", "runtime", "log", "label", "line", "box", "table",
|
|
106
|
+
"linefill", "polyline", "chart", "ticker", "map", "array", "matrix",
|
|
107
|
+
"session", "alert",
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
# Built-ins that take numbers, strings or conditions only: TradingView refuses
|
|
111
|
+
# an array or a matrix for any of their arguments (CE10123; math.abs, nz,
|
|
112
|
+
# plot, a strategy quantity, ta.sma, label.new's text and log.info's message
|
|
113
|
+
# were checked: fixtures/array_history_tv README).
|
|
114
|
+
_SCALAR_FUNCTIONS = frozenset({
|
|
115
|
+
"nz", "fixnan", "plot", "plotshape", "plotchar", "plotarrow", "plotbar",
|
|
116
|
+
"plotcandle", "bgcolor", "barcolor", "fill", "hline", "alert",
|
|
117
|
+
"alertcondition", "int", "float", "bool", "string", "color", "max_bars_back",
|
|
118
|
+
"timestamp", "time", "time_close", "year", "month", "weekofyear",
|
|
119
|
+
"dayofmonth", "dayofweek", "hour", "minute", "second",
|
|
120
|
+
})
|
|
121
|
+
_SCALAR_NAMESPACES = frozenset({
|
|
122
|
+
"math", "ta", "strategy", "input", "color", "timeframe", "syminfo",
|
|
123
|
+
"ticker", "label", "line", "box", "table", "linefill", "runtime",
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
# The uses (``Use.tag``; an entry also covers its ``:``-extensions) the
|
|
127
|
+
# earlier lowering of a read the checker does not support could not compile,
|
|
128
|
+
# from a probe of every use and every array function in every scope on the
|
|
129
|
+
# build before this one; a use not listed keeps that lowering. A variable's
|
|
130
|
+
# history read an element of the current array -- a number, a bool, a color
|
|
131
|
+
# or a string -- and a matrix variable's a ``Series<double>`` that never
|
|
132
|
+
# compiled; a parameter's read the parameter's current collection.
|
|
133
|
+
#
|
|
134
|
+
# A number's element took no array function; ``array.copy`` built a vector
|
|
135
|
+
# of the element's size, which compiled where a namespace call or a loop's
|
|
136
|
+
# local took it, not where a method or a member did.
|
|
137
|
+
_NUMBER_ELEMENT_FAILING_FUNCTIONS = frozenset({
|
|
138
|
+
"abs", "avg", "binary_search", "binary_search_leftmost",
|
|
139
|
+
"binary_search_rightmost", "concat", "covariance", "every", "first", "get",
|
|
140
|
+
"includes", "indexof", "join", "last", "lastindexof", "max", "median",
|
|
141
|
+
"min", "mode", "percentile_linear_interpolation",
|
|
142
|
+
"percentile_nearest_rank", "percentrank", "range", "size", "some",
|
|
143
|
+
"sort_indices", "standardize", "stdev", "sum", "variance", "add_row",
|
|
144
|
+
"add_col",
|
|
145
|
+
})
|
|
146
|
+
# The array functions returning a new array: whether the earlier lowering
|
|
147
|
+
# compiled depends on how that array is used (``_array_result_consumer``):
|
|
148
|
+
# called a method on, returned by a function, rendered or held by a member,
|
|
149
|
+
# it did not compile.
|
|
150
|
+
_ARRAY_RESULT_FUNCTIONS = frozenset({
|
|
151
|
+
"abs", "copy", "slice", "sort_indices", "standardize", "concat",
|
|
152
|
+
})
|
|
153
|
+
_FAILING_RESULT_CONSUMERS = ("receiver", "result", "render", "member")
|
|
154
|
+
_ELEMENT_LOWERING_FAILS = frozenset({
|
|
155
|
+
"read:receiver", "change:receiver", "slice:receiver", "slice:argument",
|
|
156
|
+
"change:argument", "copy:typed", "loop", "read:history", "other:collection",
|
|
157
|
+
}) | frozenset(f"read:argument:{f}" for f in _NUMBER_ELEMENT_FAILING_FUNCTIONS) | frozenset(
|
|
158
|
+
f"read:argument:copy:{c}" for c in _FAILING_RESULT_CONSUMERS)
|
|
159
|
+
# A ``std::string`` element has ``size()``, ``[]``, ``clear()``, iterators and
|
|
160
|
+
# more, so many array functions compiled on it; it has no na and no number
|
|
161
|
+
# rendering.
|
|
162
|
+
_STRING_ELEMENT_FAILING_FUNCTIONS = frozenset({
|
|
163
|
+
"binary_search", "binary_search_leftmost", "binary_search_rightmost",
|
|
164
|
+
"concat", "copy", "covariance", "includes", "indexof", "lastindexof",
|
|
165
|
+
"add_row", "add_col",
|
|
166
|
+
})
|
|
167
|
+
_STRING_ELEMENT_LOWERING_FAILS = frozenset({
|
|
168
|
+
"read:receiver", "change:receiver", "slice:receiver", "copy:typed", "na",
|
|
169
|
+
"render:tostring", "read:history", "other:collection",
|
|
170
|
+
"change:argument:push", "change:argument:unshift", "change:argument:insert",
|
|
171
|
+
"change:argument:set", "change:argument:fill",
|
|
172
|
+
# A function returning what these read (typed a number).
|
|
173
|
+
"change:argument:pop:result", "change:argument:shift:result",
|
|
174
|
+
"change:argument:remove:result", "read:argument:get:result",
|
|
175
|
+
"read:argument:first:result", "read:argument:last:result",
|
|
176
|
+
"read:argument:join:result",
|
|
177
|
+
}) | frozenset(f"read:argument:{f}" for f in _STRING_ELEMENT_FAILING_FUNCTIONS) | frozenset(
|
|
178
|
+
f"{how}:argument:{f}:{c}"
|
|
179
|
+
for how, f in (("read", "abs"), ("read", "sort_indices"), ("read", "standardize"),
|
|
180
|
+
("change", "concat"), ("slice", "slice"))
|
|
181
|
+
for c in _FAILING_RESULT_CONSUMERS)
|
|
182
|
+
# A parameter's current array took most array functions; one returning a new
|
|
183
|
+
# array, a method on it, na() and rendering did not compile, nor did a
|
|
184
|
+
# string array's element-typed reads (typed a number).
|
|
185
|
+
_PARAMETER_LOWERING_FAILS = frozenset({
|
|
186
|
+
"read:receiver", "change:receiver", "slice:receiver", "na", "render",
|
|
187
|
+
"copy:untyped",
|
|
188
|
+
}) | frozenset(
|
|
189
|
+
f"{how}:argument:{f}:{c}"
|
|
190
|
+
for how, f in (("read", "abs"), ("read", "copy"), ("read", "sort_indices"),
|
|
191
|
+
("read", "standardize"), ("slice", "slice"), ("change", "concat"))
|
|
192
|
+
for c in _FAILING_RESULT_CONSUMERS)
|
|
193
|
+
# A string parameter's element read where a variable or a function's result
|
|
194
|
+
# typed a number took it.
|
|
195
|
+
_STRING_PARAMETER_LOWERING_FAILS = _PARAMETER_LOWERING_FAILS | frozenset(
|
|
196
|
+
f"{how}:argument:{f}:{c}"
|
|
197
|
+
for how, names in (
|
|
198
|
+
("read", ("avg", "covariance", "every", "first", "get", "join", "last",
|
|
199
|
+
"max", "median", "min", "mode", "percentile_linear_interpolation",
|
|
200
|
+
"percentile_nearest_rank", "percentrank", "range", "some",
|
|
201
|
+
"stdev", "sum", "variance")),
|
|
202
|
+
("change", ("pop", "remove", "shift")))
|
|
203
|
+
for f in names for c in ("result", "bound"))
|
|
204
|
+
_MATRIX_RECEIVER_LOWERING_FAILS = frozenset({
|
|
205
|
+
"read:receiver", "change:receiver", "render", "copy:untyped",
|
|
206
|
+
"read:argument:det", "read:argument:copy", "change:argument",
|
|
207
|
+
"other:collection", # matrix.sum (_matrix_sum_use)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _failing_tag(tags, table) -> str | None:
|
|
212
|
+
"""The first of ``tags`` the table lists (an entry covers its
|
|
213
|
+
``:``-extensions), else None."""
|
|
214
|
+
for tag in sorted(tags):
|
|
215
|
+
if any(tag == entry or tag.startswith(entry + ":") for entry in table):
|
|
216
|
+
return tag
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _element_name(spec) -> str | None:
|
|
221
|
+
"""``float`` / ``int`` / ``bool`` / ``string`` / ``color`` for a
|
|
222
|
+
collection of primitives, ``udt`` for one of objects or drawings."""
|
|
223
|
+
element = getattr(spec, "element", None)
|
|
224
|
+
if element is None:
|
|
225
|
+
return None
|
|
226
|
+
return element.name if element.kind == "primitive" else "udt"
|
|
227
|
+
|
|
228
|
+
COLLECTION_HISTORY_CPP = r"""
|
|
229
|
+
// The history of an array or a matrix variable: TradingView's x[k] is a
|
|
230
|
+
// read-only copy of the collection as the variable left it at the end of its
|
|
231
|
+
// scope's execution k executions back (tests/fixtures/array_history_tv). The
|
|
232
|
+
// variable keeps its own value; each execution of its declaration (each bar,
|
|
233
|
+
// for a var) opens a slot, and the end of the bar closes it with a copy.
|
|
234
|
+
template <typename T>
|
|
235
|
+
struct _PFCollectionTraits;
|
|
236
|
+
template <typename E, typename A>
|
|
237
|
+
struct _PFCollectionTraits<std::vector<E, A>> {
|
|
238
|
+
static std::shared_ptr<const std::vector<E, A>> freeze(const std::vector<E, A>& value) {
|
|
239
|
+
return std::make_shared<const std::vector<E, A>>(value);
|
|
240
|
+
}
|
|
241
|
+
static std::vector<E, A> copy(const std::vector<E, A>& value) { return value; }
|
|
242
|
+
static bool is_na(const std::vector<E, A>&) { return false; }
|
|
243
|
+
static const char* na_message() {
|
|
244
|
+
return "@NA_ARRAY_MESSAGE@";
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
# The matrix traits, emitted when the script uses the matrix types.
|
|
250
|
+
COLLECTION_HISTORY_MATRIX_CPP = r"""
|
|
251
|
+
template <>
|
|
252
|
+
struct _PFCollectionTraits<PineMatrix> {
|
|
253
|
+
static std::shared_ptr<const PineMatrix> freeze(const PineMatrix& value) {
|
|
254
|
+
if (value.is_na()) return nullptr;
|
|
255
|
+
return std::make_shared<const PineMatrix>(value.copy());
|
|
256
|
+
}
|
|
257
|
+
static PineMatrix copy(const PineMatrix& value) {
|
|
258
|
+
return value.is_na() ? PineMatrix{} : value.copy();
|
|
259
|
+
}
|
|
260
|
+
static bool is_na(const PineMatrix& value) { return value.is_na(); }
|
|
261
|
+
static const char* na_message() {
|
|
262
|
+
return "@NA_MATRIX_MESSAGE@";
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
COLLECTION_HISTORY_GENERIC_MATRIX_CPP = r"""
|
|
268
|
+
template <typename E>
|
|
269
|
+
struct _PFCollectionTraits<PineGenericMatrix<E>> {
|
|
270
|
+
static std::shared_ptr<const PineGenericMatrix<E>> freeze(const PineGenericMatrix<E>& value) {
|
|
271
|
+
if (value.is_na()) return nullptr;
|
|
272
|
+
return std::make_shared<const PineGenericMatrix<E>>(value.copy());
|
|
273
|
+
}
|
|
274
|
+
static PineGenericMatrix<E> copy(const PineGenericMatrix<E>& value) {
|
|
275
|
+
return value.is_na() ? PineGenericMatrix<E>{} : value.copy();
|
|
276
|
+
}
|
|
277
|
+
static bool is_na(const PineGenericMatrix<E>& value) { return value.is_na(); }
|
|
278
|
+
static const char* na_message() {
|
|
279
|
+
return "@NA_MATRIX_MESSAGE@";
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
COLLECTION_HISTORY_CLASS_CPP = r"""
|
|
285
|
+
// A change to the history of an array or a matrix stops the run, as
|
|
286
|
+
// TradingView's does (RE10051).
|
|
287
|
+
[[noreturn]] inline void _pf_collection_history_changed() {
|
|
288
|
+
pine_runtime_error("@HISTORICAL_CHANGE_MESSAGE@");
|
|
289
|
+
throw 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
template <typename T>
|
|
293
|
+
class _PFCollectionHistory {
|
|
294
|
+
public:
|
|
295
|
+
explicit _PFCollectionHistory(int max_len = 500) : slots_(max_len) {}
|
|
296
|
+
// An execution of the variable's declaration begins: the previous one's
|
|
297
|
+
// slot keeps its copy, and this one's opens.
|
|
298
|
+
void open(const T& value, bool new_slot) {
|
|
299
|
+
close(value);
|
|
300
|
+
if (new_slot) slots_.push(nullptr);
|
|
301
|
+
else slots_.update(nullptr);
|
|
302
|
+
open_ = true;
|
|
303
|
+
}
|
|
304
|
+
// The execution ended: its slot keeps a copy of the value.
|
|
305
|
+
void close(const T& value) {
|
|
306
|
+
if (!open_) return;
|
|
307
|
+
slots_.update(_PFCollectionTraits<T>::freeze(value));
|
|
308
|
+
open_ = false;
|
|
309
|
+
}
|
|
310
|
+
bool is_na(int offset) const { return offset <= 0 || !slots_[offset]; }
|
|
311
|
+
bool is_na(int offset, const T& current) const {
|
|
312
|
+
return offset == 0 ? _PFCollectionTraits<T>::is_na(current) : is_na(offset);
|
|
313
|
+
}
|
|
314
|
+
// The copy k executions back; a method on a na history stops the run, as
|
|
315
|
+
// TradingView's does (RE10052, RE10053).
|
|
316
|
+
const T& at(int offset) const {
|
|
317
|
+
std::shared_ptr<const T> slot = offset > 0 ? slots_[offset] : nullptr;
|
|
318
|
+
if (!slot) {
|
|
319
|
+
pine_runtime_error(_PFCollectionTraits<T>::na_message());
|
|
320
|
+
}
|
|
321
|
+
return *slot;
|
|
322
|
+
}
|
|
323
|
+
const T& at(int offset, const T& current) const {
|
|
324
|
+
return offset == 0 ? current : at(offset);
|
|
325
|
+
}
|
|
326
|
+
// A copy of the history for a variable or a parameter: empty for an
|
|
327
|
+
// array before it has one (PineForge holds no na array), na for a matrix.
|
|
328
|
+
T value(int offset) const {
|
|
329
|
+
std::shared_ptr<const T> slot = offset > 0 ? slots_[offset] : nullptr;
|
|
330
|
+
return slot ? _PFCollectionTraits<T>::copy(*slot) : T{};
|
|
331
|
+
}
|
|
332
|
+
T value(int offset, const T& current) const {
|
|
333
|
+
return offset == 0 ? current : value(offset);
|
|
334
|
+
}
|
|
335
|
+
// The receiver of a built-in that changes it: the variable itself at a
|
|
336
|
+
// zero offset, else the run stops, as TradingView's does (RE10051; a na
|
|
337
|
+
// history stops it with RE10052 / RE10053 first).
|
|
338
|
+
T& changed(int offset, T& current) const {
|
|
339
|
+
if (offset == 0) return current;
|
|
340
|
+
(void)at(offset);
|
|
341
|
+
_pf_collection_history_changed();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private:
|
|
345
|
+
Series<std::shared_ptr<const T>> slots_;
|
|
346
|
+
bool open_ = false;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# The templates spell TradingView's texts from the constants above.
|
|
353
|
+
_MESSAGES = {
|
|
354
|
+
"@NA_ARRAY_MESSAGE@": NA_ARRAY_MESSAGE,
|
|
355
|
+
"@NA_MATRIX_MESSAGE@": NA_MATRIX_MESSAGE,
|
|
356
|
+
"@HISTORICAL_CHANGE_MESSAGE@": HISTORICAL_CHANGE_MESSAGE,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _with_messages(text: str) -> str:
|
|
361
|
+
for placeholder, message in _MESSAGES.items():
|
|
362
|
+
text = text.replace(placeholder, message)
|
|
363
|
+
return text
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
COLLECTION_HISTORY_CPP = _with_messages(COLLECTION_HISTORY_CPP)
|
|
367
|
+
COLLECTION_HISTORY_MATRIX_CPP = _with_messages(COLLECTION_HISTORY_MATRIX_CPP)
|
|
368
|
+
COLLECTION_HISTORY_GENERIC_MATRIX_CPP = _with_messages(COLLECTION_HISTORY_GENERIC_MATRIX_CPP)
|
|
369
|
+
COLLECTION_HISTORY_CLASS_CPP = _with_messages(COLLECTION_HISTORY_CLASS_CPP)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def history_annotation(node) -> dict | None:
|
|
373
|
+
"""The ``COLLECTION_HISTORY_ANNOTATION`` of ``node``, else None."""
|
|
374
|
+
return (getattr(node, "annotations", None) or {}).get(COLLECTION_HISTORY_ANNOTATION)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def literal_offset(node: Subscript) -> int | None:
|
|
378
|
+
"""The offset of ``node`` when it is an integer literal, else None."""
|
|
379
|
+
index = node.index
|
|
380
|
+
if isinstance(index, NumberLiteral) and isinstance(index.value, int):
|
|
381
|
+
return index.value
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def collection_hint(hint) -> str | None:
|
|
386
|
+
"""``array`` / ``matrix`` / ``map`` for a collection type hint
|
|
387
|
+
(``array<float>``, ``float[]``, ``matrix<int>``), else None."""
|
|
388
|
+
if not isinstance(hint, str):
|
|
389
|
+
return None
|
|
390
|
+
if hint.endswith("[]") or hint.startswith("array<") or hint == "array":
|
|
391
|
+
return "array"
|
|
392
|
+
if hint.startswith("matrix<") or hint == "matrix":
|
|
393
|
+
return "matrix"
|
|
394
|
+
if hint.startswith("map<") or hint == "map":
|
|
395
|
+
return "map"
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@dataclass
|
|
400
|
+
class HistoryRead:
|
|
401
|
+
"""A history read of an array or a matrix variable the analyzer met."""
|
|
402
|
+
node: Subscript
|
|
403
|
+
name: str
|
|
404
|
+
kind: str # "array" | "matrix"
|
|
405
|
+
spec: object # its TypeSpec
|
|
406
|
+
in_callable: bool # read inside a function or method body
|
|
407
|
+
is_parameter: bool # the variable is a parameter
|
|
408
|
+
is_global: bool # the variable belongs to the top level
|
|
409
|
+
is_var: bool
|
|
410
|
+
decl_node_id: int | None
|
|
411
|
+
scope_name: str = "global" # the analyzer's scope of the variable
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@dataclass
|
|
415
|
+
class HistoryVariable:
|
|
416
|
+
"""A declaration whose history the script reads, for the codegen."""
|
|
417
|
+
name: str
|
|
418
|
+
kind: str
|
|
419
|
+
spec: object
|
|
420
|
+
is_var: bool
|
|
421
|
+
capacity: int | None # None: the Series default (dynamic offsets)
|
|
422
|
+
decl_node_id: int | None
|
|
423
|
+
ordinal: int = 0 # its rank among its name's declarations
|
|
424
|
+
# The other declarations writing the same class member (a sibling
|
|
425
|
+
# block's ``x``): each closes this history before it writes.
|
|
426
|
+
closed_by: set[int] = field(default_factory=set)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
@dataclass
|
|
430
|
+
class Decision:
|
|
431
|
+
outcome: str # SUPPORTED | LEGACY | REFUSED
|
|
432
|
+
use: str | None = None # SUPPORTED: read / change / copy / loop / na
|
|
433
|
+
node: object = None # REFUSED: where
|
|
434
|
+
message: str | None = None
|
|
435
|
+
hint: str | None = None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@dataclass
|
|
439
|
+
class Use:
|
|
440
|
+
"""How a value of an array or a matrix is used.
|
|
441
|
+
|
|
442
|
+
``how``: ``read`` / ``change`` (a built-in reads or changes it), ``loop``
|
|
443
|
+
(a for...in iterable), ``na``, ``render`` (``str.tostring``,
|
|
444
|
+
``str.format``: an element renders in the earlier lowering), ``copy``
|
|
445
|
+
(bound to a name or given to a parameter, whose uses ``names`` sums up),
|
|
446
|
+
``slice`` (a slice of it, whose uses ``names`` sums up), ``flow`` (a
|
|
447
|
+
selection's arm, a function's result, a request's payload: ``names``
|
|
448
|
+
sums up its consumer), ``discard`` (a value nothing reads), ``reject``
|
|
449
|
+
(TradingView refuses it) and ``other``. ``needs_collection`` marks an
|
|
450
|
+
``other`` or a ``copy`` that only a collection fits (an array-typed
|
|
451
|
+
parameter or field, a history of the history)."""
|
|
452
|
+
how: str
|
|
453
|
+
node: object = None
|
|
454
|
+
message: str | None = None
|
|
455
|
+
hint: str | None = None
|
|
456
|
+
names: "NameUses | None" = None
|
|
457
|
+
needs_collection: bool = False
|
|
458
|
+
form: str | None = None # read / change / slice: receiver or argument;
|
|
459
|
+
# render: tostring or format
|
|
460
|
+
|
|
461
|
+
def tag(self) -> str:
|
|
462
|
+
"""The use's class, by which ``_ELEMENT_LOWERING_FAILS`` and its
|
|
463
|
+
siblings tell whether the earlier lowering compiled it."""
|
|
464
|
+
if self.how in ("read", "change", "slice"):
|
|
465
|
+
return f"{self.how}:{self.form or 'argument'}"
|
|
466
|
+
if self.how == "render":
|
|
467
|
+
return f"render:{self.form or 'tostring'}"
|
|
468
|
+
if self.how == "copy":
|
|
469
|
+
return "copy:typed" if self.needs_collection else "copy:untyped"
|
|
470
|
+
if self.how == "other" and self.needs_collection:
|
|
471
|
+
return "other:collection"
|
|
472
|
+
return self.how
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@dataclass
|
|
476
|
+
class NameUses:
|
|
477
|
+
"""The uses a value reaches, summed up: the first that needs the
|
|
478
|
+
collection (``collection``), that an element fits (``element``: ``na``,
|
|
479
|
+
``str.tostring``), that changes it (``change``), that TradingView
|
|
480
|
+
refuses (``reject``) and any other (``other``)."""
|
|
481
|
+
collection: Use | None = None
|
|
482
|
+
element: Use | None = None
|
|
483
|
+
change: Use | None = None
|
|
484
|
+
reject: Use | None = None
|
|
485
|
+
other: Use | None = None
|
|
486
|
+
tags: dict[str, Use] = field(default_factory=dict) # every use's class
|
|
487
|
+
|
|
488
|
+
def merge(self, other: "NameUses | None") -> None:
|
|
489
|
+
if other is None:
|
|
490
|
+
return
|
|
491
|
+
for key in ("collection", "element", "change", "reject", "other"):
|
|
492
|
+
if getattr(self, key) is None and getattr(other, key) is not None:
|
|
493
|
+
setattr(self, key, getattr(other, key))
|
|
494
|
+
for tag, use in other.tags.items():
|
|
495
|
+
self.tags.setdefault(tag, use)
|
|
496
|
+
|
|
497
|
+
def add(self, use: Use) -> None:
|
|
498
|
+
"""Fold one use in."""
|
|
499
|
+
self.tags.setdefault(use.tag(), use)
|
|
500
|
+
how = use.how
|
|
501
|
+
if how in ("read", "loop"):
|
|
502
|
+
self.merge(NameUses(collection=use))
|
|
503
|
+
elif how == "change":
|
|
504
|
+
self.merge(NameUses(collection=use, change=use))
|
|
505
|
+
elif how in ("na", "render"):
|
|
506
|
+
self.merge(NameUses(element=use))
|
|
507
|
+
elif how == "reject":
|
|
508
|
+
self.merge(NameUses(reject=use))
|
|
509
|
+
elif how == "slice":
|
|
510
|
+
# The slice's own uses are an array's: only what it changes or
|
|
511
|
+
# TradingView refuses reaches the history.
|
|
512
|
+
self.merge(NameUses(collection=use))
|
|
513
|
+
if use.names is not None:
|
|
514
|
+
self.merge(NameUses(change=use.names.change,
|
|
515
|
+
reject=use.names.reject))
|
|
516
|
+
if use.names.element is not None or use.names.other is not None:
|
|
517
|
+
self.merge(NameUses(other=use.names.element or use.names.other))
|
|
518
|
+
elif how in ("copy", "flow"):
|
|
519
|
+
self.merge(use.names)
|
|
520
|
+
if use.needs_collection:
|
|
521
|
+
self.merge(NameUses(collection=use))
|
|
522
|
+
elif how == "other":
|
|
523
|
+
self.merge(NameUses(other=use))
|
|
524
|
+
if use.needs_collection:
|
|
525
|
+
self.merge(NameUses(collection=use))
|
|
526
|
+
|
|
527
|
+
def needs_collection(self) -> bool:
|
|
528
|
+
return self.collection is not None or self.change is not None
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _a(kind: str) -> str:
|
|
532
|
+
"""``an array`` / ``a matrix``."""
|
|
533
|
+
return f"an {kind}" if kind[:1] in "aeiou" else f"a {kind}"
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _spelling(node: Subscript) -> str:
|
|
537
|
+
obj = node.object
|
|
538
|
+
name = obj.name if isinstance(obj, Identifier) else "x"
|
|
539
|
+
offset = literal_offset(node)
|
|
540
|
+
index = (str(offset) if offset is not None
|
|
541
|
+
else node.index.name if isinstance(node.index, Identifier) else "k")
|
|
542
|
+
return f"{name}[{index}]"
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _body_lists(node) -> list[list]:
|
|
546
|
+
"""The statement lists directly under ``node``."""
|
|
547
|
+
if isinstance(node, (Program, FuncDef, MethodDef, ForStmt, ForInStmt, WhileStmt)):
|
|
548
|
+
return [node.body]
|
|
549
|
+
if isinstance(node, IfStmt):
|
|
550
|
+
return [node.body, node.else_body]
|
|
551
|
+
if isinstance(node, SwitchStmt):
|
|
552
|
+
return [body for _cond, body in node.cases] + [node.default_body]
|
|
553
|
+
return []
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _declares(stmt, name: str) -> bool:
|
|
557
|
+
if isinstance(stmt, VarDecl):
|
|
558
|
+
return stmt.name == name
|
|
559
|
+
if isinstance(stmt, TupleAssign):
|
|
560
|
+
return name in stmt.names
|
|
561
|
+
return False
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class CollectionHistoryChecker:
|
|
565
|
+
"""Decides every array or matrix history read (see the module
|
|
566
|
+
docstring): ``decide`` gives a read its outcome, memoized by node."""
|
|
567
|
+
|
|
568
|
+
def __init__(self, program: Program, name_kind=None):
|
|
569
|
+
"""``name_kind(name)`` answers ``array``, ``matrix``, ``map`` or
|
|
570
|
+
``scalar`` for a script variable the analyzer typed, else None."""
|
|
571
|
+
from .ast_nodes import TypeDecl
|
|
572
|
+
from .codegen.tables import ARRAY_METHODS, MATRIX_METHODS
|
|
573
|
+
|
|
574
|
+
self._name_kind = name_kind or (lambda name: None)
|
|
575
|
+
self._types = {stmt.name: stmt for stmt in program.body if isinstance(stmt, TypeDecl)}
|
|
576
|
+
self._array_methods = frozenset(ARRAY_METHODS)
|
|
577
|
+
self._matrix_methods = frozenset(MATRIX_METHODS)
|
|
578
|
+
self._parent: dict[int, object] = {}
|
|
579
|
+
self._nodes_by_id: dict[int, object] = {}
|
|
580
|
+
self._program = program
|
|
581
|
+
for node, _depth in iter_ast_nodes(program):
|
|
582
|
+
self._nodes_by_id[id(node)] = node
|
|
583
|
+
for child in syntax_children(node):
|
|
584
|
+
self._parent[id(child)] = node
|
|
585
|
+
self._functions: dict[str, list[FuncDef]] = {}
|
|
586
|
+
self._methods: dict[str, list[MethodDef]] = {}
|
|
587
|
+
for stmt in program.body:
|
|
588
|
+
if isinstance(stmt, FuncDef):
|
|
589
|
+
self._functions.setdefault(stmt.name, []).append(stmt)
|
|
590
|
+
elif isinstance(stmt, MethodDef):
|
|
591
|
+
self._methods.setdefault(stmt.name, []).append(stmt)
|
|
592
|
+
self._top_level_ids = {id(stmt) for stmt in program.body}
|
|
593
|
+
# Indexes built once, so a name's reads and a callable's calls cost
|
|
594
|
+
# their own count, not a walk of the script each.
|
|
595
|
+
self._position: dict[int, tuple[object, list, int]] = {}
|
|
596
|
+
self._declaring: dict[tuple[int, str], list[int]] = {}
|
|
597
|
+
self._reads_by_name: dict[str, list[Identifier]] = {}
|
|
598
|
+
self._calls_by_name: dict[tuple[bool, str], list[FuncCall]] = {}
|
|
599
|
+
for node in self._nodes_by_id.values():
|
|
600
|
+
for body in _body_lists(node):
|
|
601
|
+
for index, stmt in enumerate(body):
|
|
602
|
+
self._position[id(stmt)] = (node, body, index)
|
|
603
|
+
for name in ([stmt.name] if isinstance(stmt, VarDecl)
|
|
604
|
+
else stmt.names if isinstance(stmt, TupleAssign) else []):
|
|
605
|
+
self._declaring.setdefault((id(body), name), []).append(index)
|
|
606
|
+
if isinstance(node, Identifier):
|
|
607
|
+
self._reads_by_name.setdefault(node.name, []).append(node)
|
|
608
|
+
elif isinstance(node, FuncCall):
|
|
609
|
+
callee = node.callee
|
|
610
|
+
if isinstance(callee, Identifier):
|
|
611
|
+
self._calls_by_name.setdefault((False, callee.name), []).append(node)
|
|
612
|
+
elif isinstance(callee, MemberAccess):
|
|
613
|
+
self._calls_by_name.setdefault((True, callee.member), []).append(node)
|
|
614
|
+
# Every read of a name, under each statement list holding it at any
|
|
615
|
+
# depth: a region's candidates are its own list's, so sibling blocks
|
|
616
|
+
# declaring one name do not scan each other's reads.
|
|
617
|
+
self._reads_under: dict[tuple[int, str], list[Identifier]] = {}
|
|
618
|
+
for name, reads in self._reads_by_name.items():
|
|
619
|
+
for read in reads:
|
|
620
|
+
current = read
|
|
621
|
+
while current is not None:
|
|
622
|
+
position = self._position.get(id(current))
|
|
623
|
+
if position is not None:
|
|
624
|
+
self._reads_under.setdefault((id(position[1]), name), []).append(read)
|
|
625
|
+
current = self._parent.get(id(current))
|
|
626
|
+
self._emitted = self._emitted_callables()
|
|
627
|
+
self._decisions: dict[int, Decision] = {}
|
|
628
|
+
self._uses: dict[tuple, Use] = {}
|
|
629
|
+
self._name_uses: dict[tuple, NameUses] = {}
|
|
630
|
+
self._in_progress: set[tuple] = set()
|
|
631
|
+
# The element type of the collection being decided (``float``,
|
|
632
|
+
# ``string``, ``color``, ``udt``...): an element renders or reads na
|
|
633
|
+
# by its type in the earlier lowering.
|
|
634
|
+
self._element: str | None = None
|
|
635
|
+
|
|
636
|
+
# -- the program's structure ---------------------------------------------
|
|
637
|
+
|
|
638
|
+
def _ancestors(self, node):
|
|
639
|
+
current = self._parent.get(id(node))
|
|
640
|
+
while current is not None:
|
|
641
|
+
yield current
|
|
642
|
+
current = self._parent.get(id(current))
|
|
643
|
+
|
|
644
|
+
def _callable_of(self, node):
|
|
645
|
+
"""The function or method whose body holds ``node``, else None."""
|
|
646
|
+
return next((a for a in self._ancestors(node)
|
|
647
|
+
if isinstance(a, (FuncDef, MethodDef))), None)
|
|
648
|
+
|
|
649
|
+
def _emitted_callables(self) -> set[int]:
|
|
650
|
+
"""The functions and methods the codegen emits: every method, called
|
|
651
|
+
or not, and every function a call names anywhere -- a call in a
|
|
652
|
+
function nothing calls included, since the analyzer visits every
|
|
653
|
+
body and registers each function it sees called. A function no call
|
|
654
|
+
names is never emitted, so anything in its body compiled."""
|
|
655
|
+
emitted = {id(definition) for defs in self._methods.values()
|
|
656
|
+
for definition in defs}
|
|
657
|
+
for name, defs in self._functions.items():
|
|
658
|
+
if self._calls_by_name.get((False, name)):
|
|
659
|
+
emitted.update(id(definition) for definition in defs)
|
|
660
|
+
return emitted
|
|
661
|
+
|
|
662
|
+
def is_dead(self, node) -> bool:
|
|
663
|
+
"""Whether ``node`` sits in a function the codegen does not emit."""
|
|
664
|
+
owner = self._callable_of(node)
|
|
665
|
+
return owner is not None and id(owner) not in self._emitted
|
|
666
|
+
|
|
667
|
+
def _in_request(self, node) -> bool:
|
|
668
|
+
return any(isinstance(a, FuncCall) and isinstance(a.callee, MemberAccess)
|
|
669
|
+
and isinstance(a.callee.object, Identifier)
|
|
670
|
+
and a.callee.object.name == "request"
|
|
671
|
+
for a in self._ancestors(node))
|
|
672
|
+
|
|
673
|
+
def declaration_scope(self, decl_node_id: int | None) -> str:
|
|
674
|
+
"""``top`` for a declaration statement of the script's top level,
|
|
675
|
+
``block`` for one in an if or switch block of it, ``loop`` inside a
|
|
676
|
+
loop, ``callable`` inside a function or method, ``unknown`` without a
|
|
677
|
+
declaration statement (a tuple's, a loop binder's)."""
|
|
678
|
+
if decl_node_id is None:
|
|
679
|
+
return "unknown"
|
|
680
|
+
decl = self._nodes_by_id.get(decl_node_id)
|
|
681
|
+
if not isinstance(decl, VarDecl):
|
|
682
|
+
return "unknown"
|
|
683
|
+
if id(decl) in self._top_level_ids:
|
|
684
|
+
return "top"
|
|
685
|
+
for ancestor in self._ancestors(decl):
|
|
686
|
+
if isinstance(ancestor, (FuncDef, MethodDef)):
|
|
687
|
+
return "callable"
|
|
688
|
+
if isinstance(ancestor, (ForStmt, ForInStmt, WhileStmt)):
|
|
689
|
+
return "loop"
|
|
690
|
+
return "block"
|
|
691
|
+
|
|
692
|
+
def _containing_list(self, stmt) -> tuple[object, list] | None:
|
|
693
|
+
position = self._position.get(id(stmt))
|
|
694
|
+
return None if position is None else position[:2]
|
|
695
|
+
|
|
696
|
+
# -- the reads of a name ---------------------------------------------------
|
|
697
|
+
|
|
698
|
+
def _in_region(self, read, name: str, region: list, start: int,
|
|
699
|
+
top: bool) -> bool:
|
|
700
|
+
"""Whether ``read`` (an identifier spelled ``name``) reads the
|
|
701
|
+
variable a declaration at ``region[start - 1]`` binds -- for a
|
|
702
|
+
parameter, ``start`` 0 of its callable's body: it sits in
|
|
703
|
+
``region[start:]`` before a later declaration of the name there, and
|
|
704
|
+
no nested statement list or loop between them declares the name
|
|
705
|
+
first. A declaration of the script's top level (``top``) also
|
|
706
|
+
reaches a function's or a method's reads that no parameter, and no
|
|
707
|
+
declaration before them in their own or an enclosing list of that
|
|
708
|
+
body, binds (one in a later or a sibling block does not)."""
|
|
709
|
+
current = read
|
|
710
|
+
while True:
|
|
711
|
+
position = self._position.get(id(current))
|
|
712
|
+
if position is not None:
|
|
713
|
+
holder, body, index = position
|
|
714
|
+
declared = self._declaring.get((id(body), name), [])
|
|
715
|
+
if body is region:
|
|
716
|
+
later = next((i for i in declared if i >= start), None)
|
|
717
|
+
return index >= start and (later is None or index <= later)
|
|
718
|
+
if isinstance(holder, (FuncDef, MethodDef)):
|
|
719
|
+
# The script variable, unless a parameter or a
|
|
720
|
+
# declaration before the read in the body binds the name
|
|
721
|
+
# (one in a later or nested block does not).
|
|
722
|
+
return (top and name not in holder.params
|
|
723
|
+
and not (declared and declared[0] < index))
|
|
724
|
+
if declared and declared[0] < index:
|
|
725
|
+
return False
|
|
726
|
+
if body is getattr(holder, "body", None) and (
|
|
727
|
+
(isinstance(holder, ForStmt) and holder.var == name)
|
|
728
|
+
or (isinstance(holder, ForInStmt)
|
|
729
|
+
and (holder.var == name or name in (holder.vars or [])))):
|
|
730
|
+
return False
|
|
731
|
+
current = self._parent.get(id(current))
|
|
732
|
+
if current is None:
|
|
733
|
+
return False
|
|
734
|
+
|
|
735
|
+
def _region_reads(self, name: str, region: list, start: int, top: bool) -> list:
|
|
736
|
+
"""The reads of ``name`` in a region (``_in_region``), in source order."""
|
|
737
|
+
reads = [read for read in self._reads_under.get((id(region), name), ())
|
|
738
|
+
if self._in_region(read, name, region, start, top)]
|
|
739
|
+
reads.sort(key=lambda r: (getattr(r.loc, "line", 0) or 0,
|
|
740
|
+
getattr(r.loc, "column", 0) or 0))
|
|
741
|
+
return reads
|
|
742
|
+
|
|
743
|
+
def _declaration_reads(self, decl) -> list:
|
|
744
|
+
"""The reads a declaration's value reaches through its name: the
|
|
745
|
+
rest of its statement list and, for the top level, the functions and
|
|
746
|
+
methods that read the script variable."""
|
|
747
|
+
position = self._position.get(id(decl))
|
|
748
|
+
if position is None:
|
|
749
|
+
return []
|
|
750
|
+
holder, body, index = position
|
|
751
|
+
return self._region_reads(decl.name, body, index + 1, isinstance(holder, Program))
|
|
752
|
+
|
|
753
|
+
def _declaration_of(self, name: str, node):
|
|
754
|
+
"""The declaration (a ``VarDecl``, or ``(definition, index)`` for a
|
|
755
|
+
parameter) that ``name`` read at ``node`` resolves to, else None."""
|
|
756
|
+
current = node
|
|
757
|
+
while current is not None:
|
|
758
|
+
parent = self._parent.get(id(current))
|
|
759
|
+
if parent is None:
|
|
760
|
+
return None
|
|
761
|
+
if isinstance(parent, (FuncDef, MethodDef)) and name in parent.params:
|
|
762
|
+
return (parent, parent.params.index(name))
|
|
763
|
+
position = self._position.get(id(current))
|
|
764
|
+
if position is not None:
|
|
765
|
+
_holder, body, index = position
|
|
766
|
+
for earlier in reversed(body[:index]):
|
|
767
|
+
if isinstance(earlier, VarDecl) and earlier.name == name:
|
|
768
|
+
return earlier
|
|
769
|
+
if _declares(earlier, name):
|
|
770
|
+
return None
|
|
771
|
+
current = parent
|
|
772
|
+
return None
|
|
773
|
+
|
|
774
|
+
# -- uses ----------------------------------------------------------------
|
|
775
|
+
|
|
776
|
+
def _call_namespace(self, call: FuncCall) -> str | None:
|
|
777
|
+
callee = call.callee
|
|
778
|
+
if isinstance(callee, MemberAccess) and isinstance(callee.object, Identifier):
|
|
779
|
+
return callee.object.name
|
|
780
|
+
return None
|
|
781
|
+
|
|
782
|
+
def _builtin_methods(self, kind: str) -> frozenset[str]:
|
|
783
|
+
return self._array_methods if kind == "array" else self._matrix_methods
|
|
784
|
+
|
|
785
|
+
def _changing(self, kind: str, method: str) -> bool:
|
|
786
|
+
table = ARRAY_CHANGING_METHODS if kind == "array" else MATRIX_CHANGING_METHODS
|
|
787
|
+
return method in table
|
|
788
|
+
|
|
789
|
+
def use(self, node, kind: str, label: str) -> Use:
|
|
790
|
+
"""How the value of ``node`` -- an array or a matrix: a history
|
|
791
|
+
read, a name bound to one, a call or a selection holding one -- is
|
|
792
|
+
used. Memoized."""
|
|
793
|
+
key = (id(node), kind, self._element)
|
|
794
|
+
cached = self._uses.get(key)
|
|
795
|
+
if cached is not None:
|
|
796
|
+
return cached
|
|
797
|
+
if key in self._in_progress:
|
|
798
|
+
return Use("discard", node)
|
|
799
|
+
self._in_progress.add(key)
|
|
800
|
+
try:
|
|
801
|
+
result = self._use(node, kind, label)
|
|
802
|
+
finally:
|
|
803
|
+
self._in_progress.discard(key)
|
|
804
|
+
self._uses[key] = result
|
|
805
|
+
return result
|
|
806
|
+
|
|
807
|
+
def _reject(self, node, message: str, hint: str | None = None) -> Use:
|
|
808
|
+
return Use("reject", node, message, hint)
|
|
809
|
+
|
|
810
|
+
def _use(self, node, kind: str, label: str) -> Use:
|
|
811
|
+
parent = self._parent.get(id(node))
|
|
812
|
+
if isinstance(parent, MemberAccess) and parent.object is node:
|
|
813
|
+
return self._receiver_use(parent, node, kind, label)
|
|
814
|
+
if isinstance(parent, FuncCall) and parent.callee is not node:
|
|
815
|
+
return self._argument_use(parent, node, kind, label)
|
|
816
|
+
if isinstance(parent, ForInStmt) and parent.iterable is node:
|
|
817
|
+
return Use("loop", node)
|
|
818
|
+
if isinstance(parent, VarDecl) and parent.value is node:
|
|
819
|
+
return self._declaration_use(parent, node, kind, label)
|
|
820
|
+
if isinstance(parent, Assignment):
|
|
821
|
+
if parent.target is node:
|
|
822
|
+
# ``a[k] := v``: TradingView refuses it (CE10009, CE10013 for a
|
|
823
|
+
# computed offset); the earlier lowering set the current
|
|
824
|
+
# array's element and compiled, so it keeps it.
|
|
825
|
+
return Use("other", node,
|
|
826
|
+
f"{label} := ...: an assignment to the history-referencing "
|
|
827
|
+
"operator is not supported in PineForge.")
|
|
828
|
+
return self._assignment_use(parent, node, kind, label)
|
|
829
|
+
if isinstance(parent, (BinOp, UnaryOp)):
|
|
830
|
+
return self._reject(
|
|
831
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses for "
|
|
832
|
+
f"operator {parent.op} (CE10123).")
|
|
833
|
+
if isinstance(parent, Ternary):
|
|
834
|
+
if parent.condition is node:
|
|
835
|
+
return self._reject(
|
|
836
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses as "
|
|
837
|
+
"the condition of ?: (CE10123).")
|
|
838
|
+
return self._flow(parent, kind, label, node)
|
|
839
|
+
if isinstance(parent, (IfStmt, WhileStmt)) and parent.condition is node:
|
|
840
|
+
statement = "if" if isinstance(parent, IfStmt) else "while"
|
|
841
|
+
return self._reject(
|
|
842
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses as the "
|
|
843
|
+
f"condition of an {statement} statement (CE10101).")
|
|
844
|
+
if isinstance(parent, Subscript) and parent.object is node:
|
|
845
|
+
outer = NameUses()
|
|
846
|
+
outer.add(self.use(parent, kind, f"({label})[...]"))
|
|
847
|
+
return Use("other", node,
|
|
848
|
+
f"({label})[...]: the history of {_a(kind)}'s history "
|
|
849
|
+
"is not supported in PineForge.",
|
|
850
|
+
needs_collection=outer.needs_collection())
|
|
851
|
+
if isinstance(parent, ExprStmt):
|
|
852
|
+
return self._statement_use(parent, node, kind, label)
|
|
853
|
+
if isinstance(parent, TupleLiteral):
|
|
854
|
+
return self._tuple_flow(parent, node, kind, label)
|
|
855
|
+
return Use("other", node, f"{label} is not supported in PineForge here.")
|
|
856
|
+
|
|
857
|
+
def _receiver_use(self, access: MemberAccess, node, kind: str, label: str) -> Use:
|
|
858
|
+
call = self._parent.get(id(access))
|
|
859
|
+
method = access.member
|
|
860
|
+
if not (isinstance(call, FuncCall) and call.callee is access):
|
|
861
|
+
return Use("other", node, f"{label}.{method}: {_a(kind)} has no fields.",
|
|
862
|
+
needs_collection=True)
|
|
863
|
+
if method in self._builtin_methods(kind):
|
|
864
|
+
if kind == "matrix" and method == "sum":
|
|
865
|
+
return self._matrix_sum_use(node, label)
|
|
866
|
+
if kind == "array" and method == "slice":
|
|
867
|
+
return self._slice_use(call, node, label, "receiver")
|
|
868
|
+
if self._changing(kind, method):
|
|
869
|
+
return Use("change", node, form="receiver")
|
|
870
|
+
return Use("read", node, form="receiver")
|
|
871
|
+
if method in self._methods:
|
|
872
|
+
definitions = self._definitions_for(self._methods[method], 0, None, kind)
|
|
873
|
+
return Use("copy", node, names=self._parameter_uses(
|
|
874
|
+
definitions, 0, None, kind), needs_collection=True, form="receiver")
|
|
875
|
+
return Use("other", node,
|
|
876
|
+
f"{label}.{method}(): no array or matrix method of that name.",
|
|
877
|
+
needs_collection=True)
|
|
878
|
+
|
|
879
|
+
def _matrix_sum_use(self, node, label: str) -> Use:
|
|
880
|
+
"""``matrix.sum(m[k], m2)``: its matrix result does not compile,
|
|
881
|
+
history or not (the earlier build refused the history)."""
|
|
882
|
+
return Use("other", node,
|
|
883
|
+
f"matrix.sum of {label} is not supported in PineForge: the "
|
|
884
|
+
"matrix it returns does not compile.", needs_collection=True)
|
|
885
|
+
|
|
886
|
+
def _slice_use(self, call: FuncCall, node, label: str, form: str) -> Use:
|
|
887
|
+
"""``(a[k]).slice(...)``: TradingView's slice shares the history's
|
|
888
|
+
elements, so a change to it stops the run (RE10051)."""
|
|
889
|
+
names = NameUses()
|
|
890
|
+
names.add(self.use(call, "array", f"{label}.slice()"))
|
|
891
|
+
return Use("slice", node, names=names, form=form)
|
|
892
|
+
|
|
893
|
+
def _argument_use(self, call: FuncCall, node, kind: str, label: str) -> Use:
|
|
894
|
+
callee = call.callee
|
|
895
|
+
positional = next((i for i, a in enumerate(call.args) if a is node), None)
|
|
896
|
+
keyword = next((k for k, v in call.kwargs.items() if v is node), None)
|
|
897
|
+
namespace = self._call_namespace(call)
|
|
898
|
+
if isinstance(callee, Identifier):
|
|
899
|
+
name = callee.name
|
|
900
|
+
if name == "na":
|
|
901
|
+
return Use("na", node)
|
|
902
|
+
if name in self._functions:
|
|
903
|
+
definitions = self._definitions_for(
|
|
904
|
+
self._functions[name], positional, keyword, kind)
|
|
905
|
+
return Use("copy", node,
|
|
906
|
+
names=self._parameter_uses(definitions, positional, keyword, kind),
|
|
907
|
+
needs_collection=self._parameter_is_collection(
|
|
908
|
+
definitions, positional, keyword),
|
|
909
|
+
form="parameter")
|
|
910
|
+
if name in _SCALAR_FUNCTIONS:
|
|
911
|
+
return self._reject(
|
|
912
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses as an "
|
|
913
|
+
f"argument of {name} (CE10123).")
|
|
914
|
+
return Use("other", node, f"{label} as an argument of {name} is not "
|
|
915
|
+
"supported in PineForge.")
|
|
916
|
+
if not isinstance(callee, MemberAccess):
|
|
917
|
+
return Use("other", node, f"{label} is not supported in PineForge here.")
|
|
918
|
+
member = callee.member
|
|
919
|
+
if namespace in ("array", "matrix"):
|
|
920
|
+
methods = self._array_methods if namespace == "array" else self._matrix_methods
|
|
921
|
+
if member == "from" and namespace == "array":
|
|
922
|
+
return self._reject(
|
|
923
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses as an "
|
|
924
|
+
"element of array.from (CE10122).")
|
|
925
|
+
if namespace == "matrix" and member == "sum" and kind == "matrix":
|
|
926
|
+
return self._matrix_sum_use(node, label)
|
|
927
|
+
if member in methods:
|
|
928
|
+
return self._builtin_slot_use(
|
|
929
|
+
namespace, member, positional, keyword, node, kind, label)
|
|
930
|
+
return self._reject(
|
|
931
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses as an "
|
|
932
|
+
f"argument of {namespace}.{member} (CE10123).")
|
|
933
|
+
if namespace in self._types or namespace in ("chart",):
|
|
934
|
+
if member == "new":
|
|
935
|
+
return Use("other", node,
|
|
936
|
+
f"{label} as a field of a new {namespace} is not supported "
|
|
937
|
+
"in PineForge: the field would hold a changeable copy, "
|
|
938
|
+
"where TradingView's holds the read-only history.",
|
|
939
|
+
needs_collection=self._field_is_collection(
|
|
940
|
+
namespace, positional, keyword))
|
|
941
|
+
if namespace == "str":
|
|
942
|
+
if member == "tostring" and (positional == 0 or keyword == "value"):
|
|
943
|
+
return self._render_use(node, kind, label, "str.tostring")
|
|
944
|
+
if member == "format" and positional not in (None, 0):
|
|
945
|
+
return self._render_use(node, kind, label, "str.format")
|
|
946
|
+
return self._reject(
|
|
947
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses as an "
|
|
948
|
+
f"argument of str.{member} (CE10123).")
|
|
949
|
+
if namespace == "log":
|
|
950
|
+
if positional not in (None, 0):
|
|
951
|
+
return self._render_use(node, kind, label, f"log.{member}")
|
|
952
|
+
return self._reject(
|
|
953
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses as the "
|
|
954
|
+
f"message of log.{member} (CE10123).")
|
|
955
|
+
if namespace == "request":
|
|
956
|
+
return self._flow(call, kind, label, node)
|
|
957
|
+
if namespace in _SCALAR_NAMESPACES:
|
|
958
|
+
return self._reject(
|
|
959
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses as an "
|
|
960
|
+
f"argument of {namespace}.{member} (CE10123).")
|
|
961
|
+
if namespace not in _BUILTIN_NAMESPACES and member in self._methods:
|
|
962
|
+
# A user method called on another receiver: a parameter after it.
|
|
963
|
+
if len(self._methods[member]) > 1:
|
|
964
|
+
# Overloads: the earlier lowering passed the element to the
|
|
965
|
+
# one taking a number, which compiled.
|
|
966
|
+
return Use("other", node, f"{label} as an argument of the "
|
|
967
|
+
f"overloaded method {member} is not supported in "
|
|
968
|
+
"PineForge.")
|
|
969
|
+
position = None if positional is None else positional + 1
|
|
970
|
+
definitions = self._definitions_for(
|
|
971
|
+
self._methods[member], position, keyword, kind)
|
|
972
|
+
return Use("copy", node, names=self._parameter_uses(
|
|
973
|
+
definitions, position, keyword, kind),
|
|
974
|
+
needs_collection=self._parameter_is_collection(
|
|
975
|
+
definitions, position, keyword))
|
|
976
|
+
if (namespace not in _BUILTIN_NAMESPACES and namespace not in self._types
|
|
977
|
+
and (member in self._array_methods or member in self._matrix_methods)):
|
|
978
|
+
# A built-in method on another collection (``c.concat(a[1])``,
|
|
979
|
+
# ``m.add_row(0, a[1])``): its namespace position is one more.
|
|
980
|
+
spaces = [ns for ns, table in (("array", self._array_methods),
|
|
981
|
+
("matrix", self._matrix_methods))
|
|
982
|
+
if member in table]
|
|
983
|
+
receiver_kind = (self._name_kind(callee.object.name)
|
|
984
|
+
if isinstance(callee.object, Identifier) else None)
|
|
985
|
+
if receiver_kind in spaces:
|
|
986
|
+
spaces = [receiver_kind]
|
|
987
|
+
position = None if positional is None else positional + 1
|
|
988
|
+
if any(position in _COLLECTION_SLOTS.get(ns, {}).get(member, set())
|
|
989
|
+
for ns in spaces):
|
|
990
|
+
return Use("read", node, form=f"argument:{member}")
|
|
991
|
+
return self._reject(
|
|
992
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses as a "
|
|
993
|
+
f"value of {member}() (CE10123).")
|
|
994
|
+
return Use("other", node, f"{label} as an argument of "
|
|
995
|
+
f"{namespace or 'the function'}.{member} is not supported in "
|
|
996
|
+
"PineForge.")
|
|
997
|
+
|
|
998
|
+
def _builtin_slot_use(self, namespace: str, member: str, positional, keyword,
|
|
999
|
+
node, kind: str, label: str) -> Use:
|
|
1000
|
+
first = positional == 0 or keyword in ("id", "id1")
|
|
1001
|
+
form = f"argument:{member}"
|
|
1002
|
+
if first:
|
|
1003
|
+
call = self._parent[id(node)]
|
|
1004
|
+
if namespace == "array" and member in _ARRAY_RESULT_FUNCTIONS:
|
|
1005
|
+
form += self._array_result_consumer(call, f"array.{member}({label})")
|
|
1006
|
+
if namespace == "array" and member == "slice":
|
|
1007
|
+
return self._slice_use(call, node, label, form)
|
|
1008
|
+
if namespace == "array" and member not in _ARRAY_RESULT_FUNCTIONS:
|
|
1009
|
+
form += self._value_consumer(call)
|
|
1010
|
+
if self._changing(namespace, member):
|
|
1011
|
+
return Use("change", node, form=form)
|
|
1012
|
+
return Use("read", node, form=form)
|
|
1013
|
+
slots = _COLLECTION_SLOTS.get(namespace, {}).get(member, set())
|
|
1014
|
+
if positional in slots or keyword in ("id2", "array_id"):
|
|
1015
|
+
return Use("read", node, form=form)
|
|
1016
|
+
return self._reject(
|
|
1017
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses as a value "
|
|
1018
|
+
f"of {namespace}.{member} (CE10123).")
|
|
1019
|
+
|
|
1020
|
+
def _array_result_consumer(self, call: FuncCall, label: str) -> str:
|
|
1021
|
+
"""How an array function's new array (``array.copy(a[k])``) is used,
|
|
1022
|
+
for the earlier lowering's verdict (``_ARRAY_RESULT_FUNCTIONS``):
|
|
1023
|
+
``:receiver`` for a method called on it, ``:result`` for a function
|
|
1024
|
+
returning it, ``:render`` for ``str.tostring``, ``:member`` for a
|
|
1025
|
+
variable of the script's top level or a block's holding it, else
|
|
1026
|
+
none (a namespace call's or a user function's argument, a loop's or
|
|
1027
|
+
a function's local, a for...in iterable)."""
|
|
1028
|
+
consumer = self.use(call, "array", label)
|
|
1029
|
+
if consumer.form == "receiver":
|
|
1030
|
+
return ":receiver"
|
|
1031
|
+
if consumer.how == "render":
|
|
1032
|
+
return ":render"
|
|
1033
|
+
if consumer.how == "flow" and consumer.form == "result":
|
|
1034
|
+
return ":result"
|
|
1035
|
+
parent = self._parent.get(id(call))
|
|
1036
|
+
if isinstance(parent, (VarDecl, Assignment)) and self._callable_of(parent) is None \
|
|
1037
|
+
and not any(isinstance(a, (ForStmt, ForInStmt, WhileStmt))
|
|
1038
|
+
for a in self._ancestors(parent)):
|
|
1039
|
+
return ":member"
|
|
1040
|
+
return ""
|
|
1041
|
+
|
|
1042
|
+
def _value_consumer(self, call: FuncCall) -> str:
|
|
1043
|
+
"""Where an array function's value goes, for the earlier lowering's
|
|
1044
|
+
verdict on a string element or a string array parameter: ``:result``
|
|
1045
|
+
when a function or a method returns it, ``:bound`` when a variable
|
|
1046
|
+
takes it (typed a number there), else none (a statement, an
|
|
1047
|
+
argument, an operand)."""
|
|
1048
|
+
parent = self._parent.get(id(call))
|
|
1049
|
+
if isinstance(parent, ExprStmt):
|
|
1050
|
+
located = self._containing_list(parent)
|
|
1051
|
+
if (located is not None and isinstance(located[0], (FuncDef, MethodDef))
|
|
1052
|
+
and located[1][-1] is parent):
|
|
1053
|
+
return ":result"
|
|
1054
|
+
return ""
|
|
1055
|
+
if isinstance(parent, (VarDecl, Assignment)) and parent.value is call:
|
|
1056
|
+
return ":bound"
|
|
1057
|
+
return ""
|
|
1058
|
+
|
|
1059
|
+
def _render_use(self, node, kind: str, label: str, function: str) -> Use:
|
|
1060
|
+
"""``str.tostring(a[k])``, ``str.format(..., a[k])``: TradingView
|
|
1061
|
+
renders an array of numbers, bools or strings and refuses one of
|
|
1062
|
+
colors or objects (CE10123, CE10122 for str.format:
|
|
1063
|
+
fixtures/array_history_tv README); the earlier lowering rendered an
|
|
1064
|
+
element."""
|
|
1065
|
+
if self._element in ("color", "udt"):
|
|
1066
|
+
# TradingView answers CE10123 for str.tostring and CE10122 (an
|
|
1067
|
+
# argument of int, float, bool or string expected) for
|
|
1068
|
+
# str.format.
|
|
1069
|
+
what = "colors" if self._element == "color" else "objects"
|
|
1070
|
+
code = "CE10123" if function == "str.tostring" else "CE10122"
|
|
1071
|
+
return self._reject(
|
|
1072
|
+
node, f"{label} is an array of {what}, which TradingView refuses "
|
|
1073
|
+
f"for {function} ({code}).")
|
|
1074
|
+
return Use("render", node, form="tostring" if function == "str.tostring"
|
|
1075
|
+
else "format")
|
|
1076
|
+
|
|
1077
|
+
def _declaration_use(self, decl: VarDecl, node, kind: str, label: str) -> Use:
|
|
1078
|
+
if decl.type_hint in _SCALAR_HINTS:
|
|
1079
|
+
return self._reject(
|
|
1080
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses for the "
|
|
1081
|
+
f"{decl.type_hint} variable {decl.name} (CE10173).")
|
|
1082
|
+
return Use("copy", node, names=self._declaration_uses(decl, kind),
|
|
1083
|
+
needs_collection=collection_hint(decl.type_hint) is not None)
|
|
1084
|
+
|
|
1085
|
+
def _assignment_use(self, assignment: Assignment, node, kind: str, label: str) -> Use:
|
|
1086
|
+
if assignment.op != ":=":
|
|
1087
|
+
return self._reject(
|
|
1088
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses for "
|
|
1089
|
+
f"operator {assignment.op} (CE10123).")
|
|
1090
|
+
target = assignment.target
|
|
1091
|
+
if not isinstance(target, Identifier):
|
|
1092
|
+
field_type = self._field_type(target, assignment)
|
|
1093
|
+
if field_type in _SCALAR_HINTS:
|
|
1094
|
+
return self._reject(
|
|
1095
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses for "
|
|
1096
|
+
f"the {field_type} field {target.member} (CE10173).")
|
|
1097
|
+
return Use("other", node,
|
|
1098
|
+
f"{label} assigned to a field is not supported in PineForge: "
|
|
1099
|
+
"the field would hold a changeable copy, where TradingView's "
|
|
1100
|
+
"holds the read-only history.",
|
|
1101
|
+
needs_collection=collection_hint(field_type) is not None)
|
|
1102
|
+
decl = self._declaration_of(target.name, assignment)
|
|
1103
|
+
if isinstance(decl, VarDecl):
|
|
1104
|
+
if decl.type_hint in _SCALAR_HINTS:
|
|
1105
|
+
return self._reject(
|
|
1106
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses for "
|
|
1107
|
+
f"the {decl.type_hint} variable {target.name} (CE10173).")
|
|
1108
|
+
from_history = (isinstance(decl.value, Subscript)
|
|
1109
|
+
and isinstance(decl.value.object, Identifier))
|
|
1110
|
+
return Use("copy", node, names=self._declaration_uses(decl, kind),
|
|
1111
|
+
needs_collection=not from_history)
|
|
1112
|
+
if isinstance(decl, tuple):
|
|
1113
|
+
definition, index = decl
|
|
1114
|
+
return Use("copy", node, names=self._parameter_uses(
|
|
1115
|
+
[definition], index, None, kind), needs_collection=True)
|
|
1116
|
+
target_kind = self._name_kind(target.name)
|
|
1117
|
+
if target_kind == "scalar" or (target_kind is not None and target_kind != kind):
|
|
1118
|
+
return self._reject(
|
|
1119
|
+
node, f"{label} is {_a(kind)}, which TradingView refuses for the "
|
|
1120
|
+
f"variable {target.name} of another type (CE10173).")
|
|
1121
|
+
return Use("other", node, f"{label} assigned to {target.name} is not "
|
|
1122
|
+
"supported in PineForge here.")
|
|
1123
|
+
|
|
1124
|
+
def _field_type(self, target, at) -> str | None:
|
|
1125
|
+
"""The declared type of ``obj.field`` when ``obj`` is a variable
|
|
1126
|
+
whose declaration names its user-defined type, else None."""
|
|
1127
|
+
if not (isinstance(target, MemberAccess) and isinstance(target.object, Identifier)):
|
|
1128
|
+
return None
|
|
1129
|
+
decl = self._declaration_of(target.object.name, at)
|
|
1130
|
+
type_name = None
|
|
1131
|
+
if isinstance(decl, VarDecl):
|
|
1132
|
+
type_name = decl.type_hint
|
|
1133
|
+
value = decl.value
|
|
1134
|
+
if (type_name is None and isinstance(value, FuncCall)
|
|
1135
|
+
and isinstance(value.callee, MemberAccess)
|
|
1136
|
+
and isinstance(value.callee.object, Identifier)
|
|
1137
|
+
and value.callee.member in ("new", "copy")):
|
|
1138
|
+
type_name = value.callee.object.name
|
|
1139
|
+
elif isinstance(decl, tuple):
|
|
1140
|
+
definition, index = decl
|
|
1141
|
+
hints = (definition.annotations or {}).get("param_type_hints") or []
|
|
1142
|
+
type_name = hints[index] if index < len(hints) else None
|
|
1143
|
+
type_decl = self._types.get(type_name) if isinstance(type_name, str) else None
|
|
1144
|
+
if type_decl is None:
|
|
1145
|
+
return None
|
|
1146
|
+
return next((f.type_name for f in type_decl.fields if f.name == target.member), None)
|
|
1147
|
+
|
|
1148
|
+
def _statement_use(self, stmt: ExprStmt, node, kind: str, label: str) -> Use:
|
|
1149
|
+
located = self._containing_list(stmt)
|
|
1150
|
+
holder, body = located if located is not None else (None, [])
|
|
1151
|
+
last = bool(body) and body[-1] is stmt
|
|
1152
|
+
if isinstance(holder, Program):
|
|
1153
|
+
return self._reject(
|
|
1154
|
+
node, f"{label} as a statement: TradingView reads it as a "
|
|
1155
|
+
"declaration and refuses it (CE10009, \"Extraneous input\").")
|
|
1156
|
+
if not last:
|
|
1157
|
+
return Use("discard", node)
|
|
1158
|
+
if isinstance(holder, (FuncDef, MethodDef)):
|
|
1159
|
+
return self._result_use(holder, kind, label, node)
|
|
1160
|
+
if isinstance(holder, (IfStmt, SwitchStmt)):
|
|
1161
|
+
return self._flow(holder, kind, label, node)
|
|
1162
|
+
return Use("discard", node)
|
|
1163
|
+
|
|
1164
|
+
def _flow(self, holder, kind: str, label: str, node) -> Use:
|
|
1165
|
+
"""A value an expression holding ``node`` produces: a selection's
|
|
1166
|
+
arm (``c ? a[1] : b``, an if or switch block's value), a request's
|
|
1167
|
+
payload. Its consumer is the holder's."""
|
|
1168
|
+
if isinstance(holder, (IfStmt, SwitchStmt)):
|
|
1169
|
+
parent = self._parent.get(id(holder))
|
|
1170
|
+
located = self._containing_list(holder)
|
|
1171
|
+
if located is not None:
|
|
1172
|
+
outer, body = located
|
|
1173
|
+
if body[-1] is not holder:
|
|
1174
|
+
return Use("discard", node)
|
|
1175
|
+
if isinstance(outer, (FuncDef, MethodDef)):
|
|
1176
|
+
return self._result_use(outer, kind, label, node)
|
|
1177
|
+
if isinstance(outer, (IfStmt, SwitchStmt)):
|
|
1178
|
+
return self._flow(outer, kind, label, node)
|
|
1179
|
+
return Use("discard", node)
|
|
1180
|
+
if parent is None:
|
|
1181
|
+
return Use("discard", node)
|
|
1182
|
+
names = NameUses()
|
|
1183
|
+
names.add(self.use(holder, kind, label))
|
|
1184
|
+
return Use("flow", node, names=names)
|
|
1185
|
+
|
|
1186
|
+
def _tuple_flow(self, tuple_node: TupleLiteral, node, kind: str, label: str) -> Use:
|
|
1187
|
+
"""An element of a function's result tuple: each tuple declaration
|
|
1188
|
+
of a call binds it to the name at its index (``[p, q] = f()``)."""
|
|
1189
|
+
index = next(i for i, e in enumerate(tuple_node.elements) if e is node)
|
|
1190
|
+
statement = self._parent.get(id(tuple_node))
|
|
1191
|
+
located = (self._containing_list(statement)
|
|
1192
|
+
if isinstance(statement, ExprStmt) else None)
|
|
1193
|
+
if located is None or not isinstance(located[0], (FuncDef, MethodDef)) \
|
|
1194
|
+
or located[1][-1] is not statement:
|
|
1195
|
+
return Use("other", node, f"{label} in a tuple is not supported in PineForge.")
|
|
1196
|
+
definition = located[0]
|
|
1197
|
+
names = NameUses()
|
|
1198
|
+
for call in self._calls_by_name.get(
|
|
1199
|
+
(isinstance(definition, MethodDef), definition.name), ()):
|
|
1200
|
+
decl = self._parent.get(id(call))
|
|
1201
|
+
position = self._position.get(id(decl))
|
|
1202
|
+
if (not isinstance(decl, TupleAssign) or decl.value is not call
|
|
1203
|
+
or index >= len(decl.names) or position is None):
|
|
1204
|
+
continue
|
|
1205
|
+
holder, body, at = position
|
|
1206
|
+
name = decl.names[index]
|
|
1207
|
+
names.merge(self._uses_of_name(
|
|
1208
|
+
("tuple", id(decl), index, kind), name,
|
|
1209
|
+
lambda n=name, b=body, i=at, top=isinstance(holder, Program):
|
|
1210
|
+
self._region_reads(n, b, i + 1, top),
|
|
1211
|
+
kind))
|
|
1212
|
+
return Use("flow", node, names=names)
|
|
1213
|
+
|
|
1214
|
+
def _result_use(self, definition, kind: str, label: str, node) -> Use:
|
|
1215
|
+
"""A function's or a method's result: each call's use."""
|
|
1216
|
+
names = NameUses()
|
|
1217
|
+
method = isinstance(definition, MethodDef)
|
|
1218
|
+
for call in self._calls_by_name.get((method, definition.name), ()):
|
|
1219
|
+
names.add(self.use(call, kind, f"{definition.name}()"))
|
|
1220
|
+
return Use("flow", node, names=names, form="result")
|
|
1221
|
+
|
|
1222
|
+
def _definitions_for(self, definitions, index, keyword, kind: str) -> list:
|
|
1223
|
+
"""The overloads (or same-named methods of other types) whose
|
|
1224
|
+
parameter at ``index`` / ``keyword`` can take a collection of
|
|
1225
|
+
``kind``: typed as one, or untyped. All of them when none can, so a
|
|
1226
|
+
refusal still names a use."""
|
|
1227
|
+
fitting = []
|
|
1228
|
+
for definition in definitions:
|
|
1229
|
+
params = list(definition.params)
|
|
1230
|
+
position = params.index(keyword) if keyword in params else index
|
|
1231
|
+
if position is None or position >= len(params):
|
|
1232
|
+
continue
|
|
1233
|
+
hints = (definition.annotations or {}).get("param_type_hints") or []
|
|
1234
|
+
hint = hints[position] if position < len(hints) else None
|
|
1235
|
+
if isinstance(definition, MethodDef) and position == 0:
|
|
1236
|
+
hint = hint or definition.type_name
|
|
1237
|
+
if hint is None or collection_hint(hint) == kind:
|
|
1238
|
+
fitting.append(definition)
|
|
1239
|
+
return fitting or list(definitions)
|
|
1240
|
+
|
|
1241
|
+
def _parameter_is_collection(self, definitions, index, keyword) -> bool:
|
|
1242
|
+
for definition in definitions:
|
|
1243
|
+
params = list(definition.params)
|
|
1244
|
+
position = params.index(keyword) if keyword in params else index
|
|
1245
|
+
if position is None or position >= len(params):
|
|
1246
|
+
continue
|
|
1247
|
+
hints = (definition.annotations or {}).get("param_type_hints") or []
|
|
1248
|
+
hint = hints[position] if position < len(hints) else None
|
|
1249
|
+
if isinstance(definition, MethodDef) and position == 0:
|
|
1250
|
+
hint = hint or definition.type_name
|
|
1251
|
+
if collection_hint(hint) is not None:
|
|
1252
|
+
return True
|
|
1253
|
+
return False
|
|
1254
|
+
|
|
1255
|
+
def _field_is_collection(self, type_name: str, index, keyword) -> bool:
|
|
1256
|
+
decl = self._types.get(type_name)
|
|
1257
|
+
if decl is None:
|
|
1258
|
+
return False
|
|
1259
|
+
fields_ = list(decl.fields)
|
|
1260
|
+
position = next((i for i, f in enumerate(fields_) if f.name == keyword),
|
|
1261
|
+
index)
|
|
1262
|
+
if position is None or position >= len(fields_):
|
|
1263
|
+
return False
|
|
1264
|
+
return collection_hint(fields_[position].type_name) is not None
|
|
1265
|
+
|
|
1266
|
+
def _parameter_uses(self, definitions, index, keyword, kind: str) -> NameUses:
|
|
1267
|
+
names = NameUses()
|
|
1268
|
+
for definition in definitions:
|
|
1269
|
+
params = list(definition.params)
|
|
1270
|
+
position = params.index(keyword) if keyword in params else index
|
|
1271
|
+
if position is None or position >= len(params):
|
|
1272
|
+
continue
|
|
1273
|
+
key = ("param", id(definition), position, kind)
|
|
1274
|
+
names.merge(self._uses_of_name(
|
|
1275
|
+
key, params[position],
|
|
1276
|
+
lambda d=definition, p=params[position]: self._region_reads(p, d.body, 0, False),
|
|
1277
|
+
kind))
|
|
1278
|
+
return names
|
|
1279
|
+
|
|
1280
|
+
def _declaration_uses(self, decl: VarDecl, kind: str) -> NameUses:
|
|
1281
|
+
return self._uses_of_name(("decl", id(decl), kind), decl.name,
|
|
1282
|
+
lambda: self._declaration_reads(decl), kind)
|
|
1283
|
+
|
|
1284
|
+
def _uses_of_name(self, key: tuple, name: str, reads, kind: str) -> NameUses:
|
|
1285
|
+
"""The uses of a name bound to the value, summed up; memoized, and
|
|
1286
|
+
empty for a name already being summed up (a cycle of bindings)."""
|
|
1287
|
+
key = key + (self._element,)
|
|
1288
|
+
cached = self._name_uses.get(key)
|
|
1289
|
+
if cached is not None:
|
|
1290
|
+
return cached
|
|
1291
|
+
if key in self._in_progress:
|
|
1292
|
+
return NameUses()
|
|
1293
|
+
self._in_progress.add(key)
|
|
1294
|
+
try:
|
|
1295
|
+
names = NameUses()
|
|
1296
|
+
for read in reads():
|
|
1297
|
+
parent = self._parent.get(id(read))
|
|
1298
|
+
if isinstance(parent, Assignment) and parent.target is read:
|
|
1299
|
+
continue
|
|
1300
|
+
if isinstance(parent, Subscript) and parent.object is read:
|
|
1301
|
+
# The name's own history, a read of its own: the name
|
|
1302
|
+
# must hold a collection where that read needs one.
|
|
1303
|
+
own = NameUses()
|
|
1304
|
+
own.add(self.use(parent, kind, f"{name}[...]"))
|
|
1305
|
+
if own.needs_collection():
|
|
1306
|
+
names.add(Use("read", read, form="history"))
|
|
1307
|
+
continue
|
|
1308
|
+
names.add(self.use(read, kind, name))
|
|
1309
|
+
finally:
|
|
1310
|
+
self._in_progress.discard(key)
|
|
1311
|
+
self._name_uses[key] = names
|
|
1312
|
+
return names
|
|
1313
|
+
|
|
1314
|
+
# -- decisions -------------------------------------------------------------
|
|
1315
|
+
|
|
1316
|
+
def _scope_refusal(self, read: HistoryRead) -> str | None:
|
|
1317
|
+
"""Why PineForge keeps no history of the variable read, else None."""
|
|
1318
|
+
label = _spelling(read.node)
|
|
1319
|
+
if self._in_request(read.node):
|
|
1320
|
+
return (f"{label} inside a request.security expression is not "
|
|
1321
|
+
"supported in PineForge: TradingView reads the copies the "
|
|
1322
|
+
"requested timeframe's bars left, which PineForge does not keep.")
|
|
1323
|
+
if read.in_callable:
|
|
1324
|
+
what = ("parameter" if read.is_parameter
|
|
1325
|
+
else "script variable read in a function" if read.is_global
|
|
1326
|
+
else "function's local")
|
|
1327
|
+
return (f"{label}: the history of {_a(read.kind)} {what} is not "
|
|
1328
|
+
"supported in PineForge (TradingView reads one copy per "
|
|
1329
|
+
"call: fixtures/array_history_tv ahist_fn).")
|
|
1330
|
+
scope = self.declaration_scope(read.decl_node_id)
|
|
1331
|
+
if scope == "loop":
|
|
1332
|
+
return (f"{label}: the history of {_a(read.kind)} declared in a loop "
|
|
1333
|
+
"is not supported in PineForge.")
|
|
1334
|
+
if read.scope_name.startswith("top_"):
|
|
1335
|
+
return (f"{label}: the history of {_a(read.kind)} declared in a block "
|
|
1336
|
+
"that redeclares a script variable is not supported in "
|
|
1337
|
+
"PineForge (such a block's declarations are C++ locals).")
|
|
1338
|
+
if scope in ("callable", "unknown") or not read.is_global:
|
|
1339
|
+
return (f"{label}: the history of this {read.kind} is not supported "
|
|
1340
|
+
"in PineForge (only a variable the script's top level or one "
|
|
1341
|
+
"of its blocks declares keeps one).")
|
|
1342
|
+
if read.is_var and scope != "top":
|
|
1343
|
+
return (f"{label}: the history of a block's var {read.kind} is not "
|
|
1344
|
+
"supported in PineForge.")
|
|
1345
|
+
return None
|
|
1346
|
+
|
|
1347
|
+
def decide(self, read: HistoryRead) -> Decision:
|
|
1348
|
+
"""The outcome of a history read (see the module docstring)."""
|
|
1349
|
+
cached = self._decisions.get(id(read.node))
|
|
1350
|
+
if cached is not None:
|
|
1351
|
+
return cached
|
|
1352
|
+
decision = self._decide(read)
|
|
1353
|
+
self._decisions[id(read.node)] = decision
|
|
1354
|
+
return decision
|
|
1355
|
+
|
|
1356
|
+
def decision_for(self, node) -> Decision | None:
|
|
1357
|
+
return self._decisions.get(id(node))
|
|
1358
|
+
|
|
1359
|
+
def _decide(self, read: HistoryRead) -> Decision:
|
|
1360
|
+
label = _spelling(read.node)
|
|
1361
|
+
self._element = _element_name(read.spec)
|
|
1362
|
+
use = self.use(read.node, read.kind, label)
|
|
1363
|
+
uses = NameUses()
|
|
1364
|
+
uses.add(use)
|
|
1365
|
+
if self.is_dead(read.node):
|
|
1366
|
+
# The codegen emits no function no call names; a matrix's read
|
|
1367
|
+
# registers nothing (its Series made the variable a number). A
|
|
1368
|
+
# call there still types an emitted function's untyped parameter
|
|
1369
|
+
# with the earlier lowering's element, whose uses in that body
|
|
1370
|
+
# compile as they did.
|
|
1371
|
+
if (use.how == "copy" and use.form == "parameter"
|
|
1372
|
+
and not use.needs_collection and use.names is not None):
|
|
1373
|
+
# A string parameter takes na() and str.tostring(), which the
|
|
1374
|
+
# element itself did not.
|
|
1375
|
+
table = (_STRING_ELEMENT_LOWERING_FAILS - {"na", "render:tostring"}
|
|
1376
|
+
if self._element == "string" else _ELEMENT_LOWERING_FAILS)
|
|
1377
|
+
failing = _failing_tag(use.names.tags, table)
|
|
1378
|
+
if failing is not None:
|
|
1379
|
+
return Decision(REFUSED, node=read.node, message=(
|
|
1380
|
+
self._scope_refusal(read) or use.names.tags[failing].message))
|
|
1381
|
+
return Decision(LEGACY)
|
|
1382
|
+
if uses.reject is not None:
|
|
1383
|
+
return Decision(REFUSED, node=uses.reject.node or read.node,
|
|
1384
|
+
message=uses.reject.message, hint=uses.reject.hint)
|
|
1385
|
+
why = self._scope_refusal(read)
|
|
1386
|
+
if why is None:
|
|
1387
|
+
decision = self._decide_supported(read, use, uses, label)
|
|
1388
|
+
if decision is not None:
|
|
1389
|
+
return decision
|
|
1390
|
+
elif read.is_parameter:
|
|
1391
|
+
return self._decide_parameter(read, use, why)
|
|
1392
|
+
return self._decide_earlier(read, uses, label, why)
|
|
1393
|
+
|
|
1394
|
+
def _decide_supported(self, read: HistoryRead, use: Use, uses: NameUses,
|
|
1395
|
+
label: str) -> Decision | None:
|
|
1396
|
+
"""A read of a variable whose history the codegen keeps: SUPPORTED
|
|
1397
|
+
where it lowers the use, REFUSED where neither it nor the earlier
|
|
1398
|
+
lowering can, None for the earlier lowering's call."""
|
|
1399
|
+
kind = read.kind
|
|
1400
|
+
if use.how in ("read", "change", "loop", "na"):
|
|
1401
|
+
return Decision(SUPPORTED, use.how)
|
|
1402
|
+
if use.how == "slice":
|
|
1403
|
+
if uses.change is not None:
|
|
1404
|
+
return self._change_refusal(read, label, uses.change, "a slice of it")
|
|
1405
|
+
if uses.other is not None:
|
|
1406
|
+
return Decision(REFUSED, node=uses.other.node or read.node,
|
|
1407
|
+
message=uses.other.message or
|
|
1408
|
+
f"{label}.slice() is not supported in PineForge here.")
|
|
1409
|
+
return Decision(SUPPORTED, "read")
|
|
1410
|
+
if use.how == "copy":
|
|
1411
|
+
if uses.change is not None:
|
|
1412
|
+
return self._change_refusal(read, label, uses.change, "a name bound to it")
|
|
1413
|
+
if uses.needs_collection():
|
|
1414
|
+
if uses.element is not None:
|
|
1415
|
+
return Decision(
|
|
1416
|
+
REFUSED, node=read.node,
|
|
1417
|
+
message=(f"{label} is bound where the script reads it both "
|
|
1418
|
+
f"as {_a(kind)} and through na() or str.tostring(): "
|
|
1419
|
+
"that is not supported in PineForge, which holds no "
|
|
1420
|
+
f"na {kind} and does not render one."))
|
|
1421
|
+
if uses.other is not None:
|
|
1422
|
+
return Decision(REFUSED, node=uses.other.node or read.node,
|
|
1423
|
+
message=uses.other.message or
|
|
1424
|
+
f"{label} is not supported in PineForge here.")
|
|
1425
|
+
return Decision(SUPPORTED, "copy")
|
|
1426
|
+
return None
|
|
1427
|
+
if kind == "matrix" and use.how == "discard":
|
|
1428
|
+
return Decision(SUPPORTED, "copy")
|
|
1429
|
+
return None
|
|
1430
|
+
|
|
1431
|
+
def _decide_earlier(self, read: HistoryRead, uses: NameUses, label: str,
|
|
1432
|
+
why: str | None) -> Decision:
|
|
1433
|
+
"""A read the codegen does not lower: the earlier lowering (an
|
|
1434
|
+
element of the current array) where it compiled, else REFUSED."""
|
|
1435
|
+
if read.kind == "matrix":
|
|
1436
|
+
specific = uses.other.message if uses.other is not None else None
|
|
1437
|
+
return Decision(REFUSED, node=read.node, message=why or specific or (
|
|
1438
|
+
f"{label} is not supported in PineForge here: the history of a "
|
|
1439
|
+
"matrix is supported as the receiver or an argument of a matrix "
|
|
1440
|
+
"function, na(), and the value of a variable or a function's "
|
|
1441
|
+
"argument that reads it."))
|
|
1442
|
+
table = (_STRING_ELEMENT_LOWERING_FAILS if self._element == "string"
|
|
1443
|
+
else _ELEMENT_LOWERING_FAILS)
|
|
1444
|
+
failing = _failing_tag(uses.tags, table)
|
|
1445
|
+
if failing is None:
|
|
1446
|
+
return Decision(LEGACY)
|
|
1447
|
+
message = uses.tags[failing].message
|
|
1448
|
+
return Decision(REFUSED, node=read.node, message=why or message or (
|
|
1449
|
+
f"{label} is not supported in PineForge here: the history of an "
|
|
1450
|
+
"array is supported as the receiver or an argument of an array "
|
|
1451
|
+
"function, a for...in iterable, na(), and the value of a variable "
|
|
1452
|
+
"or a function's argument that reads it."))
|
|
1453
|
+
|
|
1454
|
+
def _decide_parameter(self, read: HistoryRead, use: Use, why: str) -> Decision:
|
|
1455
|
+
"""A parameter's history: the earlier lowering read the parameter's
|
|
1456
|
+
current collection, where it compiled (with a warning), else
|
|
1457
|
+
REFUSED."""
|
|
1458
|
+
owner = self._callable_of(read.node)
|
|
1459
|
+
receiver = (isinstance(owner, MethodDef) and bool(owner.params)
|
|
1460
|
+
and owner.params[0] == read.name)
|
|
1461
|
+
tags = {use.tag()}
|
|
1462
|
+
if read.kind == "matrix":
|
|
1463
|
+
if receiver and _failing_tag(tags, _MATRIX_RECEIVER_LOWERING_FAILS) is None:
|
|
1464
|
+
return Decision(LEGACY, "series")
|
|
1465
|
+
return Decision(REFUSED, node=read.node, message=why)
|
|
1466
|
+
table = (_STRING_PARAMETER_LOWERING_FAILS if self._element == "string"
|
|
1467
|
+
else _PARAMETER_LOWERING_FAILS)
|
|
1468
|
+
if _failing_tag(tags, table) is None:
|
|
1469
|
+
return Decision(LEGACY)
|
|
1470
|
+
return Decision(REFUSED, node=read.node, message=why)
|
|
1471
|
+
|
|
1472
|
+
def decide_expression(self, node: Subscript, spec) -> Decision:
|
|
1473
|
+
"""The history of an expression whose value is a collection (a
|
|
1474
|
+
selection's, a call's): the earlier lowering read an element of a
|
|
1475
|
+
selection's current array, where it compiled; a call's never
|
|
1476
|
+
compiled. Memoized by node."""
|
|
1477
|
+
cached = self._decisions.get(id(node))
|
|
1478
|
+
if cached is None:
|
|
1479
|
+
cached = self._decisions[id(node)] = self._decide_expression(node, spec)
|
|
1480
|
+
return cached
|
|
1481
|
+
|
|
1482
|
+
def _decide_expression(self, node: Subscript, spec) -> Decision:
|
|
1483
|
+
kind = spec.kind
|
|
1484
|
+
what = ("a call's" if isinstance(node.object, FuncCall)
|
|
1485
|
+
else "a selection's" if isinstance(node.object, Ternary)
|
|
1486
|
+
else "an expression's")
|
|
1487
|
+
label = "(...)[k]"
|
|
1488
|
+
self._element = _element_name(spec)
|
|
1489
|
+
use = self.use(node, kind, label)
|
|
1490
|
+
uses = NameUses()
|
|
1491
|
+
uses.add(use)
|
|
1492
|
+
if self.is_dead(node):
|
|
1493
|
+
return Decision(LEGACY)
|
|
1494
|
+
if uses.reject is not None:
|
|
1495
|
+
return Decision(REFUSED, node=uses.reject.node or node,
|
|
1496
|
+
message=uses.reject.message, hint=uses.reject.hint)
|
|
1497
|
+
table = (_STRING_ELEMENT_LOWERING_FAILS if self._element == "string"
|
|
1498
|
+
else _ELEMENT_LOWERING_FAILS)
|
|
1499
|
+
if (isinstance(node.object, Ternary) and kind == "array"
|
|
1500
|
+
and _failing_tag(uses.tags, table) is None):
|
|
1501
|
+
return Decision(LEGACY, "element")
|
|
1502
|
+
return Decision(
|
|
1503
|
+
REFUSED, node=node,
|
|
1504
|
+
message=(f"The history of {what} {kind} is not supported in "
|
|
1505
|
+
"PineForge here: TradingView reads the copy that expression "
|
|
1506
|
+
"produced that many bars back, which PineForge does not "
|
|
1507
|
+
"keep."),
|
|
1508
|
+
hint="Bind the expression to a variable and read the variable's history.")
|
|
1509
|
+
|
|
1510
|
+
def decide_parameter_history(self, node: Subscript, spec) -> Decision:
|
|
1511
|
+
"""``x[k]`` of a parameter an array or a matrix reaches: a typed
|
|
1512
|
+
parameter as ``decide`` decides it; an untyped one's earlier lowering
|
|
1513
|
+
never compiled."""
|
|
1514
|
+
cached = self._decisions.get(id(node))
|
|
1515
|
+
if cached is not None:
|
|
1516
|
+
return cached
|
|
1517
|
+
owner = self._callable_of(node)
|
|
1518
|
+
name = node.object.name
|
|
1519
|
+
if owner is None or name not in owner.params:
|
|
1520
|
+
return Decision(LEGACY)
|
|
1521
|
+
index = owner.params.index(name)
|
|
1522
|
+
hints = (owner.annotations or {}).get("param_type_hints") or []
|
|
1523
|
+
hint = hints[index] if index < len(hints) else None
|
|
1524
|
+
if isinstance(owner, MethodDef) and index == 0:
|
|
1525
|
+
hint = hint or owner.type_name
|
|
1526
|
+
read = HistoryRead(
|
|
1527
|
+
node=node, name=name, kind=spec.kind, spec=spec, in_callable=True,
|
|
1528
|
+
is_parameter=True, is_global=False, is_var=False, decl_node_id=None,
|
|
1529
|
+
scope_name="")
|
|
1530
|
+
decision = self.decide(read)
|
|
1531
|
+
if (decision.outcome == LEGACY and collection_hint(hint) is None
|
|
1532
|
+
and not self.is_dead(node)):
|
|
1533
|
+
decision = Decision(REFUSED, node=node, message=self._scope_refusal(read))
|
|
1534
|
+
self._decisions[id(node)] = decision
|
|
1535
|
+
return decision
|
|
1536
|
+
|
|
1537
|
+
def _change_refusal(self, read: HistoryRead, label: str, change: Use,
|
|
1538
|
+
through: str) -> Decision:
|
|
1539
|
+
line = getattr(getattr(change.node, "loc", None), "line", None)
|
|
1540
|
+
return Decision(
|
|
1541
|
+
REFUSED, node=read.node,
|
|
1542
|
+
message=(f"{label} reaches a change through {through} "
|
|
1543
|
+
f"(line {line if line is not None else '?'}): TradingView "
|
|
1544
|
+
"stops the run there (RE10051: \"Cannot modify the elements "
|
|
1545
|
+
"of a historical array\"); PineForge, which holds a copy, "
|
|
1546
|
+
"does not."),
|
|
1547
|
+
hint=f"Copy it first (array.copy({label}) or matrix.copy()) to change it.")
|
|
1548
|
+
|
|
1549
|
+
# -- the codegen's history members -------------------------------------------
|
|
1550
|
+
|
|
1551
|
+
def _source_position(self, node_id: int | None) -> tuple:
|
|
1552
|
+
loc = getattr(self._nodes_by_id.get(node_id), "loc", None)
|
|
1553
|
+
return (getattr(loc, "line", 0) or 0, getattr(loc, "column", 0) or 0)
|
|
1554
|
+
|
|
1555
|
+
def history_variables(self, reads: list[HistoryRead],
|
|
1556
|
+
writers: dict[str, set[int]]) -> dict[str, HistoryVariable]:
|
|
1557
|
+
"""The declarations whose history the supported reads need, keyed
|
|
1558
|
+
``name`` (``name#<ordinal>`` for a later declaration of the name);
|
|
1559
|
+
each read's annotation names its key (``member``)."""
|
|
1560
|
+
by_declaration: dict[tuple, HistoryVariable] = {}
|
|
1561
|
+
keyed: list[tuple[dict, tuple]] = []
|
|
1562
|
+
for read in reads:
|
|
1563
|
+
decision = self._decisions.get(id(read.node))
|
|
1564
|
+
annotation = history_annotation(read.node)
|
|
1565
|
+
if decision is None or decision.outcome != SUPPORTED or annotation is None:
|
|
1566
|
+
continue
|
|
1567
|
+
offset = literal_offset(read.node)
|
|
1568
|
+
if decision.use == "loop" or offset == 0:
|
|
1569
|
+
continue
|
|
1570
|
+
key = (read.name, read.decl_node_id)
|
|
1571
|
+
entry = by_declaration.get(key)
|
|
1572
|
+
if entry is None:
|
|
1573
|
+
entry = by_declaration[key] = HistoryVariable(
|
|
1574
|
+
name=read.name, kind=read.kind, spec=read.spec,
|
|
1575
|
+
is_var=read.is_var, capacity=0, decl_node_id=read.decl_node_id)
|
|
1576
|
+
if entry.capacity is not None:
|
|
1577
|
+
entry.capacity = (None if offset is None
|
|
1578
|
+
else max(entry.capacity, offset + 1))
|
|
1579
|
+
keyed.append((annotation, key))
|
|
1580
|
+
names: dict[str, list[HistoryVariable]] = {}
|
|
1581
|
+
for entry in by_declaration.values():
|
|
1582
|
+
names.setdefault(entry.name, []).append(entry)
|
|
1583
|
+
variables: dict[str, HistoryVariable] = {}
|
|
1584
|
+
keys: dict[tuple, str] = {}
|
|
1585
|
+
for name, entries in names.items():
|
|
1586
|
+
entries.sort(key=lambda e: self._source_position(e.decl_node_id))
|
|
1587
|
+
for ordinal, entry in enumerate(entries):
|
|
1588
|
+
entry.ordinal = ordinal
|
|
1589
|
+
entry.closed_by = set(writers.get(name, set())) - {entry.decl_node_id}
|
|
1590
|
+
member_key = name if ordinal == 0 else f"{name}#{ordinal}"
|
|
1591
|
+
variables[member_key] = entry
|
|
1592
|
+
keys[(name, entry.decl_node_id)] = member_key
|
|
1593
|
+
for annotation, key in keyed:
|
|
1594
|
+
annotation["member"] = keys[key]
|
|
1595
|
+
return variables
|