@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.
- package/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +189 -1
- package/pineforge_codegen/analyzer/contracts.py +3 -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/collection_history.py +200 -0
- package/pineforge_codegen/codegen/emit_top.py +32 -5
- package/pineforge_codegen/codegen/helpers.py +5 -1
- 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/parser.py +19 -4
- package/pineforge_codegen/support_checker.py +37 -0
- package/pineforge_codegen-1.0.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""The C++ of an array's or a matrix's history (``a[k]``, ``m[k]``).
|
|
2
|
+
|
|
3
|
+
The analyzer annotates every such read with how its value is used and lists
|
|
4
|
+
the variables whose history the script reads (``ctx.collection_history``;
|
|
5
|
+
pineforge_codegen/collection_history.py). Such a variable keeps its own
|
|
6
|
+
``std::vector`` or matrix member, so every other operation on it keeps its
|
|
7
|
+
C++; beside it, ``_pf_collection_hist_<name>`` keeps the copies its
|
|
8
|
+
executions left. Each execution of the declaration -- each bar, for a top
|
|
9
|
+
level ``var`` -- opens a slot (``open``) and the end of the bar closes it with
|
|
10
|
+
a copy (``close``), so ``a[1]`` is the array as the previous bar left it,
|
|
11
|
+
TradingView's read-only history (fixtures/array_history_tv).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from ..collection_history import (
|
|
17
|
+
COLLECTION_HISTORY_CLASS_CPP,
|
|
18
|
+
COLLECTION_HISTORY_CPP,
|
|
19
|
+
COLLECTION_HISTORY_GENERIC_MATRIX_CPP,
|
|
20
|
+
COLLECTION_HISTORY_MATRIX_CPP,
|
|
21
|
+
history_annotation,
|
|
22
|
+
literal_offset,
|
|
23
|
+
)
|
|
24
|
+
from ..ast_nodes import FuncCall, Identifier, MemberAccess
|
|
25
|
+
from ..symbols import TypeSpec
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CollectionHistoryEmitter:
|
|
29
|
+
"""CodeGen mixin: the history members of array and matrix variables."""
|
|
30
|
+
|
|
31
|
+
def _collection_history_variables(self) -> list:
|
|
32
|
+
"""The declarations whose history the script reads, in key order."""
|
|
33
|
+
history = getattr(self.ctx, "collection_history", None) or {}
|
|
34
|
+
return [history[key] for key in sorted(history)]
|
|
35
|
+
|
|
36
|
+
def _collection_history_member(self, variable) -> str:
|
|
37
|
+
"""``_pf_collection_hist_<name>``; a later declaration of the same
|
|
38
|
+
name (a sibling block's) gets ``_pf_collection_hist_<ordinal>_<name>``,
|
|
39
|
+
which no Pine name (none starts with a digit) can spell: ``x``'s
|
|
40
|
+
second declaration beside a variable ``x_1``."""
|
|
41
|
+
prefix = f"{variable.ordinal}_" if variable.ordinal else ""
|
|
42
|
+
return f"_pf_collection_hist_{prefix}{self._safe_name(variable.name)}"
|
|
43
|
+
|
|
44
|
+
def _collection_history_of(self, annotation):
|
|
45
|
+
history = getattr(self.ctx, "collection_history", None) or {}
|
|
46
|
+
return history.get(annotation.get("member"))
|
|
47
|
+
|
|
48
|
+
def _collection_history_cpp_type(self, variable) -> str:
|
|
49
|
+
"""The history's element type: the variable member's own C++ type,
|
|
50
|
+
whatever its declaration spells (a wide int array included)."""
|
|
51
|
+
return f"decltype({self._safe_name(variable.name)})"
|
|
52
|
+
|
|
53
|
+
def _collection_history_capacity(self, variable) -> str:
|
|
54
|
+
"""The ring's size: the largest literal offset read plus one, or the
|
|
55
|
+
Series default (a ``max_bars_back`` cap) for a dynamic offset."""
|
|
56
|
+
if variable.capacity is None:
|
|
57
|
+
return self._series_decl_suffix()
|
|
58
|
+
return f"{{{max(2, variable.capacity)}}}"
|
|
59
|
+
|
|
60
|
+
def _emit_collection_history_helper(self, lines: list[str]) -> None:
|
|
61
|
+
variables = self._collection_history_variables()
|
|
62
|
+
if not variables:
|
|
63
|
+
return
|
|
64
|
+
float_spec = TypeSpec.primitive("float")
|
|
65
|
+
matrices = [v.spec for v in variables if v.kind == "matrix"]
|
|
66
|
+
lines.append(COLLECTION_HISTORY_CPP.strip("\n"))
|
|
67
|
+
if any(spec.element == float_spec for spec in matrices):
|
|
68
|
+
lines.append(COLLECTION_HISTORY_MATRIX_CPP.strip("\n"))
|
|
69
|
+
if any(spec.element != float_spec for spec in matrices):
|
|
70
|
+
lines.append(COLLECTION_HISTORY_GENERIC_MATRIX_CPP.strip("\n"))
|
|
71
|
+
lines.append(COLLECTION_HISTORY_CLASS_CPP.strip("\n"))
|
|
72
|
+
lines.append("")
|
|
73
|
+
|
|
74
|
+
def _collection_history_open(self, variable) -> str:
|
|
75
|
+
"""The statement opening a declaration's slot."""
|
|
76
|
+
return (f"{self._collection_history_member(variable)}.open("
|
|
77
|
+
f"{self._safe_name(variable.name)}, history_advances_new_bar());")
|
|
78
|
+
|
|
79
|
+
def _collection_history_close(self, variable) -> str:
|
|
80
|
+
return (f"{self._collection_history_member(variable)}.close("
|
|
81
|
+
f"{self._safe_name(variable.name)});")
|
|
82
|
+
|
|
83
|
+
def _collection_history_decl_statements(self, node) -> list[str]:
|
|
84
|
+
"""What an execution of a declaration does first: close the history
|
|
85
|
+
of every other declaration writing the same member (a sibling
|
|
86
|
+
block's ``x``, which keeps the array its block left), then open its
|
|
87
|
+
own slot when it is not a ``var`` (a block's on the bars it runs)."""
|
|
88
|
+
statements = []
|
|
89
|
+
own = None
|
|
90
|
+
for variable in self._collection_history_variables():
|
|
91
|
+
if id(node) in variable.closed_by:
|
|
92
|
+
statements.append(self._collection_history_close(variable))
|
|
93
|
+
elif variable.decl_node_id == id(node):
|
|
94
|
+
own = variable
|
|
95
|
+
if own is not None and not (getattr(node, "is_var", False)
|
|
96
|
+
or getattr(node, "is_varip", False)):
|
|
97
|
+
statements.append(self._collection_history_open(own))
|
|
98
|
+
return statements
|
|
99
|
+
|
|
100
|
+
def _emit_collection_history_var_opens(self, lines: list[str], pad: str) -> None:
|
|
101
|
+
"""A top-level ``var``'s slot opens on every bar, after its first
|
|
102
|
+
bar's initialization."""
|
|
103
|
+
for variable in self._collection_history_variables():
|
|
104
|
+
if variable.is_var:
|
|
105
|
+
lines.append(f"{pad}{self._collection_history_open(variable)}")
|
|
106
|
+
|
|
107
|
+
def _emit_collection_history_closes(self, lines: list[str], pad: str) -> None:
|
|
108
|
+
"""The bar's end: every open slot keeps a copy of its variable."""
|
|
109
|
+
for variable in self._collection_history_variables():
|
|
110
|
+
lines.append(f"{pad}{self._collection_history_close(variable)}")
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _roots_at_collection_history(value) -> bool:
|
|
114
|
+
"""Whether ``value`` is an array's or a matrix's history or a
|
|
115
|
+
built-in's call on one (``m[1]``, ``(m[1]).copy()``,
|
|
116
|
+
``matrix.transpose(m[1])``)."""
|
|
117
|
+
if history_annotation(value) is not None:
|
|
118
|
+
return True
|
|
119
|
+
if not isinstance(value, FuncCall):
|
|
120
|
+
return False
|
|
121
|
+
callee = value.callee
|
|
122
|
+
if isinstance(callee, MemberAccess) and history_annotation(callee.object) is not None:
|
|
123
|
+
return True
|
|
124
|
+
first = value.args[0] if value.args else None
|
|
125
|
+
return (isinstance(callee, MemberAccess)
|
|
126
|
+
and isinstance(callee.object, Identifier)
|
|
127
|
+
and callee.object.name in ("array", "matrix")
|
|
128
|
+
and first is not None and history_annotation(first) is not None)
|
|
129
|
+
|
|
130
|
+
def _collection_history_offset_cpp(self, node) -> str:
|
|
131
|
+
offset = literal_offset(node)
|
|
132
|
+
if offset is not None:
|
|
133
|
+
return str(offset)
|
|
134
|
+
return self._history_offset_cpp(self._visit_expr(node.index), node.index)
|
|
135
|
+
|
|
136
|
+
def _lower_collection_history(self, node) -> str | None:
|
|
137
|
+
"""The C++ of an annotated array or matrix history read, by its use:
|
|
138
|
+
a ``const`` copy a built-in reads (``at``), a copy for a variable or a
|
|
139
|
+
parameter (``value``), the receiver of a built-in that changes it
|
|
140
|
+
(``changed``: the run stops past a zero offset), the variable itself
|
|
141
|
+
for a for...in loop (TradingView iterates the current array) and at a
|
|
142
|
+
zero offset. None for any other read."""
|
|
143
|
+
annotation = history_annotation(node)
|
|
144
|
+
if annotation is None:
|
|
145
|
+
return None
|
|
146
|
+
current = self._visit_expr(node.object)
|
|
147
|
+
use = annotation["use"]
|
|
148
|
+
offset = literal_offset(node)
|
|
149
|
+
if use == "loop" or offset == 0:
|
|
150
|
+
return current
|
|
151
|
+
member = self._collection_history_member(self._collection_history_of(annotation))
|
|
152
|
+
index = self._collection_history_offset_cpp(node)
|
|
153
|
+
dynamic = offset is None
|
|
154
|
+
if use == "change":
|
|
155
|
+
return f"{member}.changed({index}, {current})"
|
|
156
|
+
if use == "copy":
|
|
157
|
+
return (f"{member}.value({index}, {current})" if dynamic
|
|
158
|
+
else f"{member}.value({index})")
|
|
159
|
+
return (f"{member}.at({index}, {current})" if dynamic
|
|
160
|
+
else f"{member}.at({index})")
|
|
161
|
+
|
|
162
|
+
def _collection_history_na(self, node) -> str | None:
|
|
163
|
+
"""``na(a[k])``: whether the history holds no copy there (an array
|
|
164
|
+
variable itself is never na in PineForge; a matrix may be)."""
|
|
165
|
+
annotation = history_annotation(node)
|
|
166
|
+
if annotation is None or annotation["use"] != "na":
|
|
167
|
+
return None
|
|
168
|
+
current = self._visit_expr(node.object)
|
|
169
|
+
offset = literal_offset(node)
|
|
170
|
+
if offset == 0:
|
|
171
|
+
return f"is_na({current})" if annotation["kind"] == "matrix" else "false"
|
|
172
|
+
member = self._collection_history_member(self._collection_history_of(annotation))
|
|
173
|
+
index = self._collection_history_offset_cpp(node)
|
|
174
|
+
if offset is None:
|
|
175
|
+
return f"{member}.is_na({index}, {current})"
|
|
176
|
+
return f"{member}.is_na({index})"
|
|
177
|
+
|
|
178
|
+
def _matrix_history_method_expr(self, receiver, method: str, spec, node) -> str:
|
|
179
|
+
"""A matrix method called on a matrix's history, lowered as the
|
|
180
|
+
variable's own method call is (visit_call: a matrix variable's
|
|
181
|
+
method)."""
|
|
182
|
+
from .tables import MATRIX_METHOD_KWARGS, MATRIX_METHODS, _merge_kwargs
|
|
183
|
+
|
|
184
|
+
recv = self._visit_expr(receiver)
|
|
185
|
+
self._check_matrix_method_allowed(method, spec, node)
|
|
186
|
+
param_names = MATRIX_METHOD_KWARGS.get(method)
|
|
187
|
+
raw_args = (
|
|
188
|
+
_merge_kwargs(node.args, node.kwargs, param_names, lambda a: a)
|
|
189
|
+
if param_names and node.kwargs else list(node.args)
|
|
190
|
+
)
|
|
191
|
+
args = self._matrix_bool_value_args(
|
|
192
|
+
method, [self._visit_expr(a) for a in raw_args], raw_args, spec)
|
|
193
|
+
try:
|
|
194
|
+
return MATRIX_METHODS[method](recv, args)
|
|
195
|
+
except IndexError:
|
|
196
|
+
self._codegen_error(
|
|
197
|
+
node,
|
|
198
|
+
f"matrix.{method}: wrong number of arguments",
|
|
199
|
+
hint="Check Pine v6 matrix method signature (positional vs keyword).",
|
|
200
|
+
)
|
|
@@ -1721,7 +1721,12 @@ class TopLevelEmitter:
|
|
|
1721
1721
|
name,
|
|
1722
1722
|
target_cpp_type=udt_name,
|
|
1723
1723
|
)
|
|
1724
|
-
|
|
1724
|
+
if self._binding_is_series(name, safe):
|
|
1725
|
+
# A history-read var object: its first
|
|
1726
|
+
# bar's slot holds the reference.
|
|
1727
|
+
lines.append(f" {safe}.push({cpp_val});")
|
|
1728
|
+
else:
|
|
1729
|
+
lines.append(f" {safe} = {cpp_val};")
|
|
1725
1730
|
break
|
|
1726
1731
|
continue
|
|
1727
1732
|
if self._binding_is_series(name, safe):
|
|
@@ -1797,6 +1802,10 @@ class TopLevelEmitter:
|
|
|
1797
1802
|
# c. Push non-var series (they start fresh each bar with a push)
|
|
1798
1803
|
# (actual push happens in visit_VarDecl when the decl is visited)
|
|
1799
1804
|
|
|
1805
|
+
# c1. A top-level var array's or matrix's history slot opens on every
|
|
1806
|
+
# bar (pineforge_codegen/collection_history.py).
|
|
1807
|
+
self._emit_collection_history_var_opens(lines, " ")
|
|
1808
|
+
|
|
1800
1809
|
# c2. First-bar TA resize: rebuild any TA object whose ctor args come
|
|
1801
1810
|
# from input-backed variables so strategy_set_input() actually changes
|
|
1802
1811
|
# the circular-buffer sizes. Emits nothing when no TA site depends on
|
|
@@ -1815,6 +1824,10 @@ class TopLevelEmitter:
|
|
|
1815
1824
|
finally:
|
|
1816
1825
|
self._clear_lazy_edge_ta_hoists()
|
|
1817
1826
|
|
|
1827
|
+
# d2. The bar's end: each array or matrix variable whose history the
|
|
1828
|
+
# script reads keeps a copy of the value it ends the bar with.
|
|
1829
|
+
self._emit_collection_history_closes(lines, " ")
|
|
1830
|
+
|
|
1818
1831
|
# e. ``// @pf-trace`` pragma block — emitted last so trace values
|
|
1819
1832
|
# reflect every assignment / strategy call made earlier in the
|
|
1820
1833
|
# bar. The block is wrapped in ``if (trace_enabled_)`` so cost
|
|
@@ -2103,11 +2116,16 @@ class TopLevelEmitter:
|
|
|
2103
2116
|
|
|
2104
2117
|
if (
|
|
2105
2118
|
receiver_spec is not None
|
|
2106
|
-
and receiver_spec.kind == "primitive"
|
|
2119
|
+
and (receiver_spec.kind == "primitive"
|
|
2120
|
+
or (receiver_spec.kind == "udt"
|
|
2121
|
+
and (receiver_spec.name in DRAWING_TYPE_TO_CPP
|
|
2122
|
+
or receiver_spec.name in self._udt_defs)))
|
|
2107
2123
|
and p in func_sv
|
|
2108
2124
|
):
|
|
2109
2125
|
# A primitive receiver used with history is a Series boundary,
|
|
2110
|
-
# just like an ordinary history-bearing UDF parameter
|
|
2126
|
+
# just like an ordinary history-bearing UDF parameter; so is
|
|
2127
|
+
# an object or drawing receiver (``this[1]`` is the receiver of
|
|
2128
|
+
# the previous call: fixtures/udt_history_tv udth_method).
|
|
2111
2129
|
elem_cpp_t = self._series_param_element_cpp_type(
|
|
2112
2130
|
fi, i, call_site_idx
|
|
2113
2131
|
)
|
|
@@ -2118,6 +2136,9 @@ class TopLevelEmitter:
|
|
|
2118
2136
|
self._current_func_series_param_types[
|
|
2119
2137
|
self._safe_name(p)
|
|
2120
2138
|
] = elem_cpp_t
|
|
2139
|
+
if receiver_spec.kind == "udt":
|
|
2140
|
+
self._udt_param_udt[self._safe_name(p)] = receiver_spec.name
|
|
2141
|
+
self._udt_param_udt[p] = receiver_spec.name
|
|
2121
2142
|
elif is_method and i == 0 and fi.udt_type_name:
|
|
2122
2143
|
# Receiver pass modes follow Pine's value/ID families. A user
|
|
2123
2144
|
# UDT is itself a numeric object-ID handle, so pass it by value:
|
|
@@ -2352,10 +2373,16 @@ class TopLevelEmitter:
|
|
|
2352
2373
|
),
|
|
2353
2374
|
)
|
|
2354
2375
|
if (drawing_name in DRAWING_TYPE_TO_CPP
|
|
2355
|
-
and self._current_func_param_types.get(
|
|
2376
|
+
and (self._current_func_param_types.get(
|
|
2356
2377
|
param, ""
|
|
2357
2378
|
).removesuffix("&").removesuffix("*")
|
|
2358
|
-
== DRAWING_TYPE_TO_CPP[drawing_name]
|
|
2379
|
+
== DRAWING_TYPE_TO_CPP[drawing_name]
|
|
2380
|
+
# A history-read drawing parameter or receiver is a
|
|
2381
|
+
# ``const Series<Box>&``: ``(x[1]).get_top()`` dispatches on
|
|
2382
|
+
# the handle it holds (udth_drawparam).
|
|
2383
|
+
or (param in self._current_func_series_params
|
|
2384
|
+
and self._current_func_series_param_types.get(param)
|
|
2385
|
+
== DRAWING_TYPE_TO_CPP[drawing_name])))
|
|
2359
2386
|
}
|
|
2360
2387
|
self._lexical_udt_types = {
|
|
2361
2388
|
param: (
|
|
@@ -345,7 +345,10 @@ CPP_RESERVED = set(
|
|
|
345
345
|
# ``_nz_v``) or initialized itself from itself (``_pna_l > close`` did not
|
|
346
346
|
# compile). ``_safe_name`` escapes an authored spelling of any of them. A
|
|
347
347
|
# temporary whose name the emitter allocates against the authored spellings
|
|
348
|
-
# itself (``__pf_map_iter_N``, ``_pf_udt_Item__pf2``) is not listed.
|
|
348
|
+
# itself (``__pf_map_iter_N``, ``_pf_udt_Item__pf2``) is not listed. An
|
|
349
|
+
# array's or a matrix's history member (``_pf_collection_hist_<name>``,
|
|
350
|
+
# ``_pf_collection_hist_<n>_<name>``) is reserved the same way: a script
|
|
351
|
+
# variable of that spelling was a second member of one name.
|
|
349
352
|
CPP_TEMPORARY_NAMES = frozenset("""
|
|
350
353
|
_nz_v _nz_y _fixnan_v _pna_l _pna_r _pfc_l _pfc_r _pfc_eq _hv _hidx _sv
|
|
351
354
|
_out _func_ret _pf_v _pf_idx_v _pf_idx_t _pf_bool_v _pf_bool_t
|
|
@@ -364,6 +367,7 @@ _CPP_TEMPORARY_PATTERN = re.compile(
|
|
|
364
367
|
r"_v\d+|_secval_\d+(?:_v\d+)?|_tuple_result_\d+|_tuple_unused_\d+|__switch_val_\d+"
|
|
365
368
|
r"|_for_(?:start|end|end_eval)_\d+|_pf_(?:str|array|round)_a\d+"
|
|
366
369
|
r"|_pf_every_bar_ta_\d+|_pf_shared_\d+_\d+|__pf_array\w*|__pf_raw_\w+"
|
|
370
|
+
r"|_pf_collection_hist_\w+"
|
|
367
371
|
)
|
|
368
372
|
|
|
369
373
|
|
|
@@ -88,7 +88,7 @@ from .helpers import na_preserving_int_cast, unary_sign_cpp
|
|
|
88
88
|
from ..security_contexts import GLOBAL_ANNOTATION, UNREACHED_ANNOTATION
|
|
89
89
|
from ..symbols import PineType, method_receiver_type_name
|
|
90
90
|
from .tables import (
|
|
91
|
-
BAR_BUILTINS, MATH_FUNC_MAP, PINE_TYPE_TO_CPP, SECURITY_BAR_FIELDS,
|
|
91
|
+
BAR_BUILTINS, DRAWING_TYPE_TO_CPP, MATH_FUNC_MAP, PINE_TYPE_TO_CPP, SECURITY_BAR_FIELDS,
|
|
92
92
|
SECURITY_BAR_FIELD_EXPRS, SECURITY_BAR_FIELD_TYPES, TA_TUPLE_FIELDS,
|
|
93
93
|
_math_minmax_na_expr, _math_round_digits_expr, _merge_kwargs,
|
|
94
94
|
)
|
|
@@ -6127,11 +6127,66 @@ class SecurityEmitter:
|
|
|
6127
6127
|
lines.append(" }")
|
|
6128
6128
|
lines.append("")
|
|
6129
6129
|
|
|
6130
|
+
def _refuse_security_reference_history(self, item: dict) -> None:
|
|
6131
|
+
"""A request.security expression that reads the history of an
|
|
6132
|
+
object or drawing reference, itself or in a function or method it
|
|
6133
|
+
calls. TradingView evaluates it on the requested bars, where the
|
|
6134
|
+
variable holds the objects of that timeframe's bars; PineForge keeps
|
|
6135
|
+
such a history on the chart's bars only, and the payload read the
|
|
6136
|
+
chart's (it never compiled before: the history was a
|
|
6137
|
+
``Series<double>``). Refused."""
|
|
6138
|
+
if item["expr_node"] is None:
|
|
6139
|
+
# ``request.security(sym, tf)`` with no expression: lowered to na
|
|
6140
|
+
# as it always was (TradingView refuses it, CE10165).
|
|
6141
|
+
return
|
|
6142
|
+
pending = [item["expr_node"]]
|
|
6143
|
+
seen: set[str] = set()
|
|
6144
|
+
while pending:
|
|
6145
|
+
for node, _depth in iter_ast_nodes(pending.pop()):
|
|
6146
|
+
if isinstance(node, Subscript):
|
|
6147
|
+
receiver = node.object
|
|
6148
|
+
kind = (self._series_handle_type_name(receiver.name)
|
|
6149
|
+
if isinstance(receiver, Identifier) else None)
|
|
6150
|
+
if kind is None:
|
|
6151
|
+
spec = self._type_spec_from_expr(receiver)
|
|
6152
|
+
if (spec is not None and spec.kind == "udt"
|
|
6153
|
+
and (spec.name in DRAWING_TYPE_TO_CPP
|
|
6154
|
+
or spec.name in self._udt_defs)):
|
|
6155
|
+
kind = spec.name
|
|
6156
|
+
if kind is not None:
|
|
6157
|
+
self._codegen_error(
|
|
6158
|
+
node,
|
|
6159
|
+
f"History of a {kind} reference inside a "
|
|
6160
|
+
"request.security expression is not supported in "
|
|
6161
|
+
"PineForge: TradingView reads the references the "
|
|
6162
|
+
"variable held on the requested timeframe's bars, "
|
|
6163
|
+
"which PineForge does not keep.",
|
|
6164
|
+
hint="Request the field values the expression "
|
|
6165
|
+
"needs (request.security(..., obj.field)) "
|
|
6166
|
+
"and read their history on the chart.",
|
|
6167
|
+
)
|
|
6168
|
+
if not isinstance(node, FuncCall):
|
|
6169
|
+
continue
|
|
6170
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
6171
|
+
keys = []
|
|
6172
|
+
if namespace is None and func_name in self._func_info_map:
|
|
6173
|
+
keys.append(func_name)
|
|
6174
|
+
if isinstance(node.callee, MemberAccess):
|
|
6175
|
+
keys.extend(key for key in self._func_info_map
|
|
6176
|
+
if key.endswith(f".{node.callee.member}"))
|
|
6177
|
+
for key in keys:
|
|
6178
|
+
body = getattr(self._func_info_map[key], "node", None)
|
|
6179
|
+
if key not in seen and body is not None:
|
|
6180
|
+
seen.add(key)
|
|
6181
|
+
pending.append(body)
|
|
6182
|
+
|
|
6130
6183
|
def _emit_security_evaluators(self, lines: list[str]) -> None:
|
|
6131
6184
|
"""Emit _eval_security_N() methods and evaluate_security() dispatch."""
|
|
6132
6185
|
if not self._security_calls:
|
|
6133
6186
|
return
|
|
6134
6187
|
|
|
6188
|
+
for item in self._security_calls:
|
|
6189
|
+
self._refuse_security_reference_history(item)
|
|
6135
6190
|
for item in self._security_calls:
|
|
6136
6191
|
self._emit_security_evaluator_requested(item, lines)
|
|
6137
6192
|
|
|
@@ -1131,6 +1131,65 @@ class TaSiteHelper:
|
|
|
1131
1131
|
)
|
|
1132
1132
|
return cache[id(node)]
|
|
1133
1133
|
|
|
1134
|
+
def _lazy_ctor_history_call(self, call) -> bool:
|
|
1135
|
+
"""A positional call of the one user function of its name whose body
|
|
1136
|
+
is a single ``T.new(...)`` of a user-defined type over
|
|
1137
|
+
``_lazy_call_history_pure_expr`` arguments of its parameters: its value
|
|
1138
|
+
is a new object of fixed fields, and TradingView runs it on every
|
|
1139
|
+
execution of its scope below a lazy edge when its history is read
|
|
1140
|
+
(fixtures/udt_history_tv udth_expr: ``(mk(bar_index * 7)[1]).v`` in a
|
|
1141
|
+
ternary's arm reads the previous bar's call)."""
|
|
1142
|
+
if not isinstance(call, FuncCall) or not isinstance(call.callee, Identifier):
|
|
1143
|
+
return False
|
|
1144
|
+
overloads = [fi for fi in self.ctx.func_infos if fi.name == call.callee.name]
|
|
1145
|
+
if call.kwargs or len(overloads) != 1 or overloads[0].node is None:
|
|
1146
|
+
return False
|
|
1147
|
+
node = overloads[0].node
|
|
1148
|
+
body = getattr(node, "body", None) or []
|
|
1149
|
+
if len(call.args) != len(node.params) or len(body) != 1:
|
|
1150
|
+
return False
|
|
1151
|
+
ctor = body[0].expr if isinstance(body[0], ExprStmt) else None
|
|
1152
|
+
if not (isinstance(ctor, FuncCall)
|
|
1153
|
+
and isinstance(ctor.callee, MemberAccess)
|
|
1154
|
+
and isinstance(ctor.callee.object, Identifier)
|
|
1155
|
+
and ctor.callee.object.name in self._udt_defs
|
|
1156
|
+
and ctor.callee.member == "new"):
|
|
1157
|
+
return False
|
|
1158
|
+
params = set(node.params)
|
|
1159
|
+
return all(self._lazy_call_history_pure_expr(arg, params)
|
|
1160
|
+
for arg in [*ctor.args, *ctor.kwargs.values()])
|
|
1161
|
+
|
|
1162
|
+
def _lazy_reference_history_object(self, obj) -> bool:
|
|
1163
|
+
"""Whether the history of the object or drawing reference ``obj``
|
|
1164
|
+
below a lazy edge is kept on every execution of its scope, as
|
|
1165
|
+
TradingView keeps it (udth_expr): a pure call or a
|
|
1166
|
+
``_lazy_ctor_history_call`` over pure arguments, a ternary over
|
|
1167
|
+
names, literals and operators, or a field of a named object (read
|
|
1168
|
+
na while the object is na, ``_lazy_history_read_cpp``)."""
|
|
1169
|
+
if self._reference_cpp_type(obj) is None:
|
|
1170
|
+
return False
|
|
1171
|
+
if isinstance(obj, FuncCall):
|
|
1172
|
+
return ((self._lazy_call_history_pure_call(obj)
|
|
1173
|
+
or self._lazy_ctor_history_call(obj))
|
|
1174
|
+
and all(self._lazy_call_history_pure_expr(arg, None)
|
|
1175
|
+
for arg in obj.args))
|
|
1176
|
+
if isinstance(obj, Ternary):
|
|
1177
|
+
return self._lazy_call_history_pure_expr(obj, None)
|
|
1178
|
+
return (isinstance(obj, MemberAccess)
|
|
1179
|
+
and isinstance(obj.object, Identifier)
|
|
1180
|
+
and self._reference_cpp_type(obj.object) is not None)
|
|
1181
|
+
|
|
1182
|
+
def _lazy_history_read_cpp(self, obj) -> str:
|
|
1183
|
+
"""The value a hoisted history read pushes: the expression, a field of
|
|
1184
|
+
a na object reading na (TradingView stops on the read itself)."""
|
|
1185
|
+
if (isinstance(obj, MemberAccess)
|
|
1186
|
+
and self._reference_cpp_type(obj.object) is not None):
|
|
1187
|
+
handle = self._reference_cpp_type(obj)
|
|
1188
|
+
receiver = self._visit_expr(obj.object)
|
|
1189
|
+
return (f"(is_na({receiver}) ? {handle}{{}} : "
|
|
1190
|
+
f"{handle}({self._visit_expr(obj)}))")
|
|
1191
|
+
return self._visit_expr(obj)
|
|
1192
|
+
|
|
1134
1193
|
def _lazy_call_history_units(self, stmt) -> list:
|
|
1135
1194
|
"""The ``pure_call(...)[k]`` reads below a lazy edge of ``stmt``'s
|
|
1136
1195
|
roots (``_lazy_edge_statement_roots``), in evaluation order, whose
|
|
@@ -1159,6 +1218,10 @@ class TaSiteHelper:
|
|
|
1159
1218
|
for arg in call.args)):
|
|
1160
1219
|
units.append(expr)
|
|
1161
1220
|
return
|
|
1221
|
+
if (under_lazy and self._registered_history_handle(expr) is not None
|
|
1222
|
+
and self._lazy_reference_history_object(call)):
|
|
1223
|
+
units.append(expr)
|
|
1224
|
+
return
|
|
1162
1225
|
scan(expr.object, under_lazy)
|
|
1163
1226
|
scan(expr.index, under_lazy)
|
|
1164
1227
|
elif isinstance(expr, FuncCall):
|
|
@@ -1219,7 +1282,8 @@ class TaSiteHelper:
|
|
|
1219
1282
|
hoisted = []
|
|
1220
1283
|
for read in units:
|
|
1221
1284
|
member = self._inline_history_member("hist_call", read)
|
|
1222
|
-
self._emit_history_series_write(
|
|
1285
|
+
self._emit_history_series_write(
|
|
1286
|
+
lines, pad, member, self._lazy_history_read_cpp(read.object))
|
|
1223
1287
|
self._hoisted_hist_reads[id(read)] = member
|
|
1224
1288
|
hoisted.append(id(read))
|
|
1225
1289
|
return hoisted
|