@pineforge/codegen-pyodide 1.0.0-rc.1 → 1.0.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.
@@ -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