@pineforge/codegen-pyodide 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -3
- package/README.md +2 -1
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +6 -5
- package/pineforge_codegen/analyzer/base.py +3 -3
- package/pineforge_codegen/analyzer/types.py +7 -3
- package/pineforge_codegen/codegen/base.py +36 -17
- package/pineforge_codegen/codegen/capabilities.py +143 -10
- package/pineforge_codegen/codegen/checked_settings.py +161 -4
- package/pineforge_codegen/codegen/collection_history.py +163 -12
- package/pineforge_codegen/codegen/emit_top.py +29 -13
- package/pineforge_codegen/codegen/helpers.py +25 -1
- package/pineforge_codegen/codegen/host_members.py +1 -0
- package/pineforge_codegen/codegen/input.py +23 -8
- package/pineforge_codegen/codegen/order_shapes.py +356 -0
- package/pineforge_codegen/codegen/run_stops.py +52 -0
- package/pineforge_codegen/codegen/security.py +66 -10
- package/pineforge_codegen/codegen/tables.py +144 -28
- package/pineforge_codegen/codegen/tv_number_format.py +9 -2
- package/pineforge_codegen/codegen/types.py +85 -4
- package/pineforge_codegen/codegen/visit_call.py +235 -55
- package/pineforge_codegen/codegen/visit_expr.py +40 -8
- package/pineforge_codegen/codegen/visit_stmt.py +5 -4
- package/pineforge_codegen/collection_history.py +122 -20
- package/pineforge_codegen/diagnostics_catalog.json +1 -0
- package/pineforge_codegen/external_requests.py +30 -9
- package/pineforge_codegen/matrix_overloads.py +7 -0
- package/pineforge_codegen/parser.py +7 -2
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +101 -10
- package/pineforge_codegen-1.4.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -3
- package/pineforge_codegen-1.2.0.tar.gz +0 -0
|
@@ -87,6 +87,8 @@ classes from ``..ast_nodes``.
|
|
|
87
87
|
|
|
88
88
|
from __future__ import annotations
|
|
89
89
|
|
|
90
|
+
from .run_stops import request_stop
|
|
91
|
+
|
|
90
92
|
from ..errors import Phase
|
|
91
93
|
from ..symbols import TypeSpec
|
|
92
94
|
from ..external_requests import UNPINNED_ANNOTATION
|
|
@@ -311,12 +313,18 @@ class ExprVisitor:
|
|
|
311
313
|
while isinstance(links[-1].false_val, Ternary):
|
|
312
314
|
links.append(links[-1].false_val)
|
|
313
315
|
string_na = self._ternary_string_na(links)
|
|
316
|
+
array_history_type = self._array_history_value_expr_cpp_type(node)
|
|
314
317
|
if len(links) < self._FLAT_TERNARY_CHAIN:
|
|
315
318
|
c = self._coerce_bool_expr(
|
|
316
319
|
self._visit_expr(node.condition), node.condition
|
|
317
320
|
)
|
|
318
321
|
t = self._ternary_arm(node.true_val, string_na)
|
|
319
322
|
f = self._ternary_arm(node.false_val, string_na)
|
|
323
|
+
if array_history_type is not None:
|
|
324
|
+
t = (f"{array_history_type}{{}}" if self._is_na_expr(node.true_val)
|
|
325
|
+
else f"{array_history_type}({t})")
|
|
326
|
+
f = (f"{array_history_type}{{}}" if self._is_na_expr(node.false_val)
|
|
327
|
+
else f"{array_history_type}({f})")
|
|
320
328
|
return f"(({c}) ? ({t}) : ({f}))"
|
|
321
329
|
arms = []
|
|
322
330
|
for link in links:
|
|
@@ -324,8 +332,15 @@ class ExprVisitor:
|
|
|
324
332
|
self._visit_expr(link.condition), link.condition
|
|
325
333
|
)
|
|
326
334
|
t = self._ternary_arm(link.true_val, string_na)
|
|
335
|
+
if array_history_type is not None:
|
|
336
|
+
t = (f"{array_history_type}{{}}" if self._is_na_expr(link.true_val)
|
|
337
|
+
else f"{array_history_type}({t})")
|
|
327
338
|
arms.append(f"({c}) ? ({t}) : ")
|
|
328
|
-
|
|
339
|
+
tail = self._ternary_arm(links[-1].false_val, string_na)
|
|
340
|
+
if array_history_type is not None:
|
|
341
|
+
tail = (f"{array_history_type}{{}}" if self._is_na_expr(links[-1].false_val)
|
|
342
|
+
else f"{array_history_type}({tail})")
|
|
343
|
+
return f"({''.join(arms)}({tail}))"
|
|
329
344
|
|
|
330
345
|
def _ternary_string_na(self, links: list[Ternary]) -> bool:
|
|
331
346
|
"""A ``?:`` chain whose every value arm but a bare ``na`` is a
|
|
@@ -493,15 +508,14 @@ class ExprVisitor:
|
|
|
493
508
|
value = self._visit_expr(node)
|
|
494
509
|
finally:
|
|
495
510
|
node.annotations = notes
|
|
496
|
-
|
|
511
|
+
stop = request_stop(marker)
|
|
512
|
+
if "ref" in marker:
|
|
497
513
|
# Evaluated first: a recorded request sets its flag where it is
|
|
498
514
|
# evaluated, which may be this read.
|
|
499
|
-
stop = (f'pine_runtime_error(std::string('
|
|
500
|
-
f'"{self._cpp_string_escape(marker["message"])}"))')
|
|
501
515
|
return (f"([&]() {{ auto _pf_read = {value}; "
|
|
502
516
|
f"if ({self._request_data_missing(marker['ref'])}) {stop}; "
|
|
503
517
|
f"return _pf_read; }}())")
|
|
504
|
-
return (f'([&]() {{
|
|
518
|
+
return (f'([&]() {{ {stop}; '
|
|
505
519
|
f"return {value}; }}())")
|
|
506
520
|
|
|
507
521
|
def _identifier_reads_series(self, node: Identifier) -> bool:
|
|
@@ -952,6 +966,7 @@ class ExprVisitor:
|
|
|
952
966
|
if node.member == "isfirst":
|
|
953
967
|
return "(bar_index_ == 0)"
|
|
954
968
|
if node.member == "islast":
|
|
969
|
+
self._order_shape_host_reads.add("barstate_islast_")
|
|
955
970
|
return "barstate_islast_"
|
|
956
971
|
if node.member == "isnew":
|
|
957
972
|
return "is_first_tick()"
|
|
@@ -962,6 +977,7 @@ class ExprVisitor:
|
|
|
962
977
|
if node.member == "isrealtime":
|
|
963
978
|
return "false"
|
|
964
979
|
if node.member == "islastconfirmedhistory":
|
|
980
|
+
self._order_shape_host_reads.add("barstate_islast_")
|
|
965
981
|
return "barstate_islast_"
|
|
966
982
|
return "false"
|
|
967
983
|
if ns in ("backadjustment", "settlement_as_close"):
|
|
@@ -1046,12 +1062,16 @@ class ExprVisitor:
|
|
|
1046
1062
|
return ismarket
|
|
1047
1063
|
return f"(!{ismarket} && {predicate})"
|
|
1048
1064
|
if node.member == "isfirstbar":
|
|
1065
|
+
self._order_shape_host_reads.add("session_isfirstbar_")
|
|
1049
1066
|
return "session_isfirstbar_"
|
|
1050
1067
|
if node.member == "islastbar":
|
|
1068
|
+
self._order_shape_host_reads.add("session_islastbar_")
|
|
1051
1069
|
return "session_islastbar_"
|
|
1052
1070
|
if node.member == "isfirstbar_regular":
|
|
1071
|
+
self._order_shape_host_reads.add("session_isfirstbar_regular_")
|
|
1053
1072
|
return "session_isfirstbar_regular_"
|
|
1054
1073
|
if node.member == "islastbar_regular":
|
|
1074
|
+
self._order_shape_host_reads.add("session_islastbar_regular_")
|
|
1055
1075
|
return "session_islastbar_regular_"
|
|
1056
1076
|
return "false"
|
|
1057
1077
|
if ns == "syminfo":
|
|
@@ -1083,7 +1103,7 @@ class ExprVisitor:
|
|
|
1083
1103
|
if ns in SKIP_NAMESPACES:
|
|
1084
1104
|
return "0"
|
|
1085
1105
|
if ns == "currency":
|
|
1086
|
-
return f'std::string("{node.member}")'
|
|
1106
|
+
return f'std::string("{self._cpp_string_escape(node.member)}")'
|
|
1087
1107
|
if ns == "order":
|
|
1088
1108
|
# order.ascending / order.descending. Unknown member -> "ascending".
|
|
1089
1109
|
return ORDER_DIRECTION_MAP.get(node.member, 'std::string("ascending")')
|
|
@@ -1163,7 +1183,7 @@ class ExprVisitor:
|
|
|
1163
1183
|
return f"{safe}.{node.member}"
|
|
1164
1184
|
if name not in self.ctx.series_vars:
|
|
1165
1185
|
# Unknown identifier — likely an enum value
|
|
1166
|
-
return f'std::string("{node.member}")'
|
|
1186
|
+
return f'std::string("{self._cpp_string_escape(node.member)}")'
|
|
1167
1187
|
return f"{obj}.{node.member}"
|
|
1168
1188
|
|
|
1169
1189
|
def _operand_na_kind(self, node, cpp_type: str) -> str | None:
|
|
@@ -1954,7 +1974,13 @@ class ExprVisitor:
|
|
|
1954
1974
|
"collection IDs, so this result can differ from TradingView "
|
|
1955
1975
|
"and a missing index is not represented faithfully.",
|
|
1956
1976
|
)
|
|
1957
|
-
|
|
1977
|
+
receiver = self._collection_receiver_expr(name)
|
|
1978
|
+
if spec.kind == "array":
|
|
1979
|
+
return self._legacy_array_history_element(
|
|
1980
|
+
receiver, spec, series_idx,
|
|
1981
|
+
mutable=getattr(self, "_udt_assignment_target", None) is node,
|
|
1982
|
+
)
|
|
1983
|
+
return f"{receiver}[{idx}]"
|
|
1958
1984
|
# Handle strategy.* history access (e.g., strategy.position_size[1])
|
|
1959
1985
|
if isinstance(node.object, MemberAccess):
|
|
1960
1986
|
if isinstance(node.object.object, Identifier):
|
|
@@ -2094,4 +2120,10 @@ class ExprVisitor:
|
|
|
2094
2120
|
self._warn_untracked_reference_history(node)
|
|
2095
2121
|
return obj
|
|
2096
2122
|
idx_int = self._history_offset_cpp(idx, node.index)
|
|
2123
|
+
object_spec = self._type_spec_from_expr(node.object)
|
|
2124
|
+
if object_spec is not None and object_spec.kind == "array":
|
|
2125
|
+
return self._legacy_array_history_element(
|
|
2126
|
+
obj, object_spec, idx_int,
|
|
2127
|
+
mutable=getattr(self, "_udt_assignment_target", None) is node,
|
|
2128
|
+
)
|
|
2097
2129
|
return f"{obj}[{idx_int}]"
|
|
@@ -1100,10 +1100,8 @@ class StmtVisitor:
|
|
|
1100
1100
|
# the backtest runtime. See: pineforge-codegen issue #10.
|
|
1101
1101
|
if self._is_omitted_udt_field(node.target):
|
|
1102
1102
|
recv = self._visit_expr(node.target.object)
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
f"{recv}.{node.target.member} {node.op} ... */"
|
|
1106
|
-
)
|
|
1103
|
+
omitted = self._cpp_comment_escape(f"{recv}.{node.target.member} {node.op}")
|
|
1104
|
+
lines.append(f"{pad}/* drawing field assignment omitted: {omitted} ... */")
|
|
1107
1105
|
return
|
|
1108
1106
|
# General expression target (e.g., member access)
|
|
1109
1107
|
target_cpp = self._visit_mutable_expr(node.target)
|
|
@@ -1824,6 +1822,9 @@ class StmtVisitor:
|
|
|
1824
1822
|
self._current_counted_loop_vars = (
|
|
1825
1823
|
self._current_counted_loop_vars - {node.var, *(node.vars or ())})
|
|
1826
1824
|
iterable_spec = self._type_spec_from_expr(node.iterable)
|
|
1825
|
+
if (iterable_spec is not None and iterable_spec.kind == "array"
|
|
1826
|
+
and self._array_history_value_names()):
|
|
1827
|
+
iterable = f"_pf_array_id({iterable})"
|
|
1827
1828
|
elem_spec = (
|
|
1828
1829
|
iterable_spec.element
|
|
1829
1830
|
if iterable_spec is not None and iterable_spec.kind == "array"
|
|
@@ -204,7 +204,6 @@ _STRING_PARAMETER_LOWERING_FAILS = _PARAMETER_LOWERING_FAILS | frozenset(
|
|
|
204
204
|
_MATRIX_RECEIVER_LOWERING_FAILS = frozenset({
|
|
205
205
|
"read:receiver", "change:receiver", "render", "copy:untyped",
|
|
206
206
|
"read:argument:det", "read:argument:copy", "change:argument",
|
|
207
|
-
"other:collection", # matrix.sum (_matrix_sum_use)
|
|
208
207
|
})
|
|
209
208
|
|
|
210
209
|
|
|
@@ -233,16 +232,30 @@ COLLECTION_HISTORY_CPP = r"""
|
|
|
233
232
|
// for a var) opens a slot, and the end of the bar closes it with a copy.
|
|
234
233
|
template <typename T>
|
|
235
234
|
struct _PFCollectionTraits;
|
|
235
|
+
template <typename T>
|
|
236
|
+
class _PFArrayHistoryValue;
|
|
236
237
|
template <typename E, typename A>
|
|
237
238
|
struct _PFCollectionTraits<std::vector<E, A>> {
|
|
238
239
|
static std::shared_ptr<const std::vector<E, A>> freeze(const std::vector<E, A>& value) {
|
|
239
240
|
return std::make_shared<const std::vector<E, A>>(value);
|
|
240
241
|
}
|
|
241
242
|
static std::vector<E, A> copy(const std::vector<E, A>& value) { return value; }
|
|
243
|
+
static _PFArrayHistoryValue<std::vector<E, A>> reference(std::shared_ptr<const std::vector<E, A>> value) {
|
|
244
|
+
return _PFArrayHistoryValue<std::vector<E, A>>(value);
|
|
245
|
+
}
|
|
242
246
|
static bool is_na(const std::vector<E, A>&) { return false; }
|
|
243
247
|
static const char* na_message() {
|
|
244
248
|
return "@NA_ARRAY_MESSAGE@";
|
|
245
249
|
}
|
|
250
|
+
static void na_stop() {
|
|
251
|
+
_PF_COLLECTION_STOP("na_reference", "array", "@NA_ARRAY_MESSAGE@");
|
|
252
|
+
}
|
|
253
|
+
[[noreturn]] static void history_stop() {
|
|
254
|
+
_PF_COLLECTION_STOP("historical_modified", "array", "@HISTORICAL_CHANGE_MESSAGE@");
|
|
255
|
+
#ifndef PINEFORGE_HAS_RUN_FAILURE_CODES_V1
|
|
256
|
+
throw 0;
|
|
257
|
+
#endif
|
|
258
|
+
}
|
|
246
259
|
};
|
|
247
260
|
"""
|
|
248
261
|
|
|
@@ -261,6 +274,15 @@ struct _PFCollectionTraits<PineMatrix> {
|
|
|
261
274
|
static const char* na_message() {
|
|
262
275
|
return "@NA_MATRIX_MESSAGE@";
|
|
263
276
|
}
|
|
277
|
+
static void na_stop() {
|
|
278
|
+
_PF_COLLECTION_STOP("na_reference", "matrix", "@NA_MATRIX_MESSAGE@");
|
|
279
|
+
}
|
|
280
|
+
[[noreturn]] static void history_stop() {
|
|
281
|
+
_PF_COLLECTION_STOP("historical_modified", "matrix", "@HISTORICAL_CHANGE_MESSAGE@");
|
|
282
|
+
#ifndef PINEFORGE_HAS_RUN_FAILURE_CODES_V1
|
|
283
|
+
throw 0;
|
|
284
|
+
#endif
|
|
285
|
+
}
|
|
264
286
|
};
|
|
265
287
|
"""
|
|
266
288
|
|
|
@@ -278,15 +300,83 @@ struct _PFCollectionTraits<PineGenericMatrix<E>> {
|
|
|
278
300
|
static const char* na_message() {
|
|
279
301
|
return "@NA_MATRIX_MESSAGE@";
|
|
280
302
|
}
|
|
303
|
+
static void na_stop() {
|
|
304
|
+
_PF_COLLECTION_STOP("na_reference", "matrix", "@NA_MATRIX_MESSAGE@");
|
|
305
|
+
}
|
|
306
|
+
[[noreturn]] static void history_stop() {
|
|
307
|
+
_PF_COLLECTION_STOP("historical_modified", "matrix", "@HISTORICAL_CHANGE_MESSAGE@");
|
|
308
|
+
#ifndef PINEFORGE_HAS_RUN_FAILURE_CODES_V1
|
|
309
|
+
throw 0;
|
|
310
|
+
#endif
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
COLLECTION_HISTORY_ARRAY_VALUE_CPP = r"""
|
|
316
|
+
template <typename T>
|
|
317
|
+
class _PFArrayHistoryValue {
|
|
318
|
+
public:
|
|
319
|
+
_PFArrayHistoryValue() = default;
|
|
320
|
+
_PFArrayHistoryValue(const T& value) : value_(std::make_shared<T>(value)) {}
|
|
321
|
+
_PFArrayHistoryValue(std::shared_ptr<const T> value)
|
|
322
|
+
: value_(value ? std::make_shared<T>(*value) : nullptr) {}
|
|
323
|
+
bool is_na() const { return !value_; }
|
|
324
|
+
T& get() const {
|
|
325
|
+
if (!value_) _PF_COLLECTION_STOP("na_reference", "array", "@NA_ARRAY_MESSAGE@");
|
|
326
|
+
return *value_;
|
|
327
|
+
}
|
|
328
|
+
operator T&() const { return get(); }
|
|
329
|
+
|
|
330
|
+
private:
|
|
331
|
+
std::shared_ptr<T> value_;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
template <typename T>
|
|
335
|
+
bool is_na(const _PFArrayHistoryValue<T>& value) { return value.is_na(); }
|
|
336
|
+
|
|
337
|
+
template <typename T>
|
|
338
|
+
T& _pf_array_id(T& value) { return value; }
|
|
339
|
+
template <typename T>
|
|
340
|
+
const T& _pf_array_id(const T& value) { return value; }
|
|
341
|
+
template <typename T>
|
|
342
|
+
T _pf_array_id(T&& value) { return std::move(value); }
|
|
343
|
+
template <typename T>
|
|
344
|
+
T& _pf_array_id(const _PFArrayHistoryValue<T>& value) { return value.get(); }
|
|
345
|
+
template <typename T>
|
|
346
|
+
T& _pf_array_id(_PFArrayHistoryValue<T>& value) { return value.get(); }
|
|
347
|
+
template <typename T>
|
|
348
|
+
T _pf_array_id(_PFArrayHistoryValue<T>&& value) { return value.get(); }
|
|
349
|
+
|
|
350
|
+
template <typename T>
|
|
351
|
+
struct _PFCollectionTraits<_PFArrayHistoryValue<T>> {
|
|
352
|
+
static std::shared_ptr<const _PFArrayHistoryValue<T>> freeze(const _PFArrayHistoryValue<T>& value) {
|
|
353
|
+
if (value.is_na()) return nullptr;
|
|
354
|
+
return std::make_shared<const _PFArrayHistoryValue<T>>(T(value.get()));
|
|
355
|
+
}
|
|
356
|
+
static _PFArrayHistoryValue<T> copy(const _PFArrayHistoryValue<T>& value) { return value; }
|
|
357
|
+
static _PFArrayHistoryValue<T> reference(std::shared_ptr<const _PFArrayHistoryValue<T>> value) {
|
|
358
|
+
return value ? *value : _PFArrayHistoryValue<T>{};
|
|
359
|
+
}
|
|
360
|
+
static bool is_na(const _PFArrayHistoryValue<T>& value) { return value.is_na(); }
|
|
361
|
+
static const char* na_message() { return "@NA_ARRAY_MESSAGE@"; }
|
|
362
|
+
static void na_stop() {
|
|
363
|
+
_PF_COLLECTION_STOP("na_reference", "array", "@NA_ARRAY_MESSAGE@");
|
|
364
|
+
}
|
|
365
|
+
[[noreturn]] static void history_stop() {
|
|
366
|
+
_PF_COLLECTION_STOP("historical_modified", "array", "@HISTORICAL_CHANGE_MESSAGE@");
|
|
367
|
+
#ifndef PINEFORGE_HAS_RUN_FAILURE_CODES_V1
|
|
368
|
+
throw 0;
|
|
369
|
+
#endif
|
|
370
|
+
}
|
|
281
371
|
};
|
|
282
372
|
"""
|
|
283
373
|
|
|
284
374
|
COLLECTION_HISTORY_CLASS_CPP = r"""
|
|
285
375
|
// A change to the history of an array or a matrix stops the run, as
|
|
286
376
|
// TradingView's does (RE10051).
|
|
377
|
+
template <typename T>
|
|
287
378
|
[[noreturn]] inline void _pf_collection_history_changed() {
|
|
288
|
-
|
|
289
|
-
throw 0;
|
|
379
|
+
_PFCollectionTraits<T>::history_stop();
|
|
290
380
|
}
|
|
291
381
|
|
|
292
382
|
template <typename T>
|
|
@@ -316,7 +406,7 @@ public:
|
|
|
316
406
|
const T& at(int offset) const {
|
|
317
407
|
std::shared_ptr<const T> slot = offset > 0 ? slots_[offset] : nullptr;
|
|
318
408
|
if (!slot) {
|
|
319
|
-
|
|
409
|
+
_PFCollectionTraits<T>::na_stop();
|
|
320
410
|
}
|
|
321
411
|
return *slot;
|
|
322
412
|
}
|
|
@@ -332,13 +422,20 @@ public:
|
|
|
332
422
|
T value(int offset, const T& current) const {
|
|
333
423
|
return offset == 0 ? current : value(offset);
|
|
334
424
|
}
|
|
425
|
+
auto reference(int offset) const {
|
|
426
|
+
return _PFCollectionTraits<T>::reference(offset > 0 ? slots_[offset] : nullptr);
|
|
427
|
+
}
|
|
428
|
+
auto reference(int offset, const T& current) const {
|
|
429
|
+
return offset == 0 ? _PFCollectionTraits<T>::reference(
|
|
430
|
+
_PFCollectionTraits<T>::freeze(current)) : reference(offset);
|
|
431
|
+
}
|
|
335
432
|
// The receiver of a built-in that changes it: the variable itself at a
|
|
336
433
|
// zero offset, else the run stops, as TradingView's does (RE10051; a na
|
|
337
434
|
// history stops it with RE10052 / RE10053 first).
|
|
338
435
|
T& changed(int offset, T& current) const {
|
|
339
436
|
if (offset == 0) return current;
|
|
340
437
|
(void)at(offset);
|
|
341
|
-
_pf_collection_history_changed();
|
|
438
|
+
_pf_collection_history_changed<T>();
|
|
342
439
|
}
|
|
343
440
|
|
|
344
441
|
private:
|
|
@@ -366,6 +463,7 @@ def _with_messages(text: str) -> str:
|
|
|
366
463
|
COLLECTION_HISTORY_CPP = _with_messages(COLLECTION_HISTORY_CPP)
|
|
367
464
|
COLLECTION_HISTORY_MATRIX_CPP = _with_messages(COLLECTION_HISTORY_MATRIX_CPP)
|
|
368
465
|
COLLECTION_HISTORY_GENERIC_MATRIX_CPP = _with_messages(COLLECTION_HISTORY_GENERIC_MATRIX_CPP)
|
|
466
|
+
COLLECTION_HISTORY_ARRAY_VALUE_CPP = _with_messages(COLLECTION_HISTORY_ARRAY_VALUE_CPP)
|
|
369
467
|
COLLECTION_HISTORY_CLASS_CPP = _with_messages(COLLECTION_HISTORY_CLASS_CPP)
|
|
370
468
|
|
|
371
469
|
|
|
@@ -457,6 +555,9 @@ class Use:
|
|
|
457
555
|
needs_collection: bool = False
|
|
458
556
|
form: str | None = None # read / change / slice: receiver or argument;
|
|
459
557
|
# render: tostring or format
|
|
558
|
+
crosses_callable: bool = False # a copy given to a user function's or
|
|
559
|
+
# method's parameter (a method's receiver
|
|
560
|
+
# is its first)
|
|
460
561
|
|
|
461
562
|
def tag(self) -> str:
|
|
462
563
|
"""The use's class, by which ``_ELEMENT_LOWERING_FAILS`` and its
|
|
@@ -483,11 +584,13 @@ class NameUses:
|
|
|
483
584
|
change: Use | None = None
|
|
484
585
|
reject: Use | None = None
|
|
485
586
|
other: Use | None = None
|
|
587
|
+
crosses_parameter: bool = False
|
|
486
588
|
tags: dict[str, Use] = field(default_factory=dict) # every use's class
|
|
487
589
|
|
|
488
590
|
def merge(self, other: "NameUses | None") -> None:
|
|
489
591
|
if other is None:
|
|
490
592
|
return
|
|
593
|
+
self.crosses_parameter |= other.crosses_parameter
|
|
491
594
|
for key in ("collection", "element", "change", "reject", "other"):
|
|
492
595
|
if getattr(self, key) is None and getattr(other, key) is not None:
|
|
493
596
|
setattr(self, key, getattr(other, key))
|
|
@@ -516,6 +619,7 @@ class NameUses:
|
|
|
516
619
|
if use.names.element is not None or use.names.other is not None:
|
|
517
620
|
self.merge(NameUses(other=use.names.element or use.names.other))
|
|
518
621
|
elif how in ("copy", "flow"):
|
|
622
|
+
self.crosses_parameter |= use.crosses_callable
|
|
519
623
|
self.merge(use.names)
|
|
520
624
|
if use.needs_collection:
|
|
521
625
|
self.merge(NameUses(collection=use))
|
|
@@ -861,8 +965,6 @@ class CollectionHistoryChecker:
|
|
|
861
965
|
return Use("other", node, f"{label}.{method}: {_a(kind)} has no fields.",
|
|
862
966
|
needs_collection=True)
|
|
863
967
|
if method in self._builtin_methods(kind):
|
|
864
|
-
if kind == "matrix" and method == "sum":
|
|
865
|
-
return self._matrix_sum_use(node, label)
|
|
866
968
|
if kind == "array" and method == "slice":
|
|
867
969
|
return self._slice_use(call, node, label, "receiver")
|
|
868
970
|
if self._changing(kind, method):
|
|
@@ -871,18 +973,12 @@ class CollectionHistoryChecker:
|
|
|
871
973
|
if method in self._methods:
|
|
872
974
|
definitions = self._definitions_for(self._methods[method], 0, None, kind)
|
|
873
975
|
return Use("copy", node, names=self._parameter_uses(
|
|
874
|
-
definitions, 0, None, kind), needs_collection=True, form="receiver"
|
|
976
|
+
definitions, 0, None, kind), needs_collection=True, form="receiver",
|
|
977
|
+
crosses_callable=True)
|
|
875
978
|
return Use("other", node,
|
|
876
979
|
f"{label}.{method}(): no array or matrix method of that name.",
|
|
877
980
|
needs_collection=True)
|
|
878
981
|
|
|
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
982
|
def _slice_use(self, call: FuncCall, node, label: str, form: str) -> Use:
|
|
887
983
|
"""``(a[k]).slice(...)``: TradingView's slice shares the history's
|
|
888
984
|
elements, so a change to it stops the run (RE10051)."""
|
|
@@ -906,7 +1002,7 @@ class CollectionHistoryChecker:
|
|
|
906
1002
|
names=self._parameter_uses(definitions, positional, keyword, kind),
|
|
907
1003
|
needs_collection=self._parameter_is_collection(
|
|
908
1004
|
definitions, positional, keyword),
|
|
909
|
-
form="parameter")
|
|
1005
|
+
form="parameter", crosses_callable=True)
|
|
910
1006
|
if name in _SCALAR_FUNCTIONS:
|
|
911
1007
|
return self._reject(
|
|
912
1008
|
node, f"{label} is {_a(kind)}, which TradingView refuses as an "
|
|
@@ -922,8 +1018,6 @@ class CollectionHistoryChecker:
|
|
|
922
1018
|
return self._reject(
|
|
923
1019
|
node, f"{label} is {_a(kind)}, which TradingView refuses as an "
|
|
924
1020
|
"element of array.from (CE10122).")
|
|
925
|
-
if namespace == "matrix" and member == "sum" and kind == "matrix":
|
|
926
|
-
return self._matrix_sum_use(node, label)
|
|
927
1021
|
if member in methods:
|
|
928
1022
|
return self._builtin_slot_use(
|
|
929
1023
|
namespace, member, positional, keyword, node, kind, label)
|
|
@@ -972,7 +1066,7 @@ class CollectionHistoryChecker:
|
|
|
972
1066
|
return Use("copy", node, names=self._parameter_uses(
|
|
973
1067
|
definitions, position, keyword, kind),
|
|
974
1068
|
needs_collection=self._parameter_is_collection(
|
|
975
|
-
definitions, position, keyword))
|
|
1069
|
+
definitions, position, keyword), crosses_callable=True)
|
|
976
1070
|
if (namespace not in _BUILTIN_NAMESPACES and namespace not in self._types
|
|
977
1071
|
and (member in self._array_methods or member in self._matrix_methods)):
|
|
978
1072
|
# A built-in method on another collection (``c.concat(a[1])``,
|
|
@@ -1410,6 +1504,14 @@ class CollectionHistoryChecker:
|
|
|
1410
1504
|
if use.how == "copy":
|
|
1411
1505
|
if uses.change is not None:
|
|
1412
1506
|
return self._change_refusal(read, label, uses.change, "a name bound to it")
|
|
1507
|
+
nullable = (uses.element is not None
|
|
1508
|
+
and all(not tag.startswith("render") for tag in uses.tags))
|
|
1509
|
+
# A value that reaches a user callable's parameter, directly or
|
|
1510
|
+
# through a name, stays the copy it was: the parameter is a
|
|
1511
|
+
# ``std::vector&``, which holds no na array and answers no na().
|
|
1512
|
+
parameter_copy = uses.crosses_parameter
|
|
1513
|
+
if (nullable and not parameter_copy and uses.other is None):
|
|
1514
|
+
return Decision(SUPPORTED, "reference" if kind == "array" else "copy")
|
|
1413
1515
|
if uses.needs_collection():
|
|
1414
1516
|
if uses.element is not None:
|
|
1415
1517
|
return Decision(
|
|
@@ -1422,7 +1524,7 @@ class CollectionHistoryChecker:
|
|
|
1422
1524
|
return Decision(REFUSED, node=uses.other.node or read.node,
|
|
1423
1525
|
message=uses.other.message or
|
|
1424
1526
|
f"{label} is not supported in PineForge here.")
|
|
1425
|
-
return Decision(SUPPORTED, "copy")
|
|
1527
|
+
return Decision(SUPPORTED, "reference" if kind == "array" and not parameter_copy else "copy")
|
|
1426
1528
|
return None
|
|
1427
1529
|
if kind == "matrix" and use.how == "discard":
|
|
1428
1530
|
return Decision(SUPPORTED, "copy")
|
|
@@ -357,6 +357,7 @@
|
|
|
357
357
|
"PF-E5049": {"severity": "error", "area": "codegen", "message": "session.{flag}[...] is not supported here: PineForge keeps a session flag''s history for the script''s top level, a function body and a request.security expression that reads it.", "hint": null, "explanation": "A session flag history read is supported only at the top level, in a function body or in a request.security expression, and this read is none of those.", "args": {"flag": {"kind": "identifier"}}},
|
|
358
358
|
"PF-E5050": {"severity": "error", "area": "codegen", "message": "History of the chart.point {object_name} is not supported in PineForge when the script changes its fields: PineForge holds a chart.point as a value, where TradingView''s history reads the point object as it is now.", "hint": "Keep the field in a variable of its own and read that variable''s history, or build a new point instead of changing a field.", "explanation": "History of a chart.point variable whose fields the script changes is refused because PineForge holds points as values; use a separate variable or build a new point.", "args": {"object_name": {"kind": "identifier"}}},
|
|
359
359
|
"PF-E5051": {"severity": "error", "area": "codegen", "message": "matrix ''{target_label}'' element type mismatch on reassignment: expected {target_type}, got {rhs_type}", "hint": null, "explanation": "A matrix variable is reassigned a matrix with a different element type, which Pine forbids; make the new matrix's element type match the variable's.", "args": {"target_label": {"kind": "identifier"}, "target_type": {"kind": "type"}, "rhs_type": {"kind": "type"}}},
|
|
360
|
+
"PF-E5052": {"severity": "error", "area": "codegen", "message": "array.{method}: wrong number of arguments", "hint": null, "explanation": "An array method call has too few arguments for its lowering; check the Pine v6 array method signature.", "args": {"method": {"kind": "identifier"}}},
|
|
360
361
|
"PF-E6001": {"severity": "error", "area": "collection-history", "message": "matrix.{method}: wrong number of arguments", "hint": "Check Pine v6 matrix method signature (positional vs keyword).", "explanation": "A matrix method call has the wrong number of arguments; check the Pine v6 matrix method signature and whether arguments are positional or keyword.", "args": {"method": {"kind": "identifier"}}},
|
|
361
362
|
"PF-E6002": {"severity": "error", "area": "collection-history", "message": "{label} := ...: an assignment to the history-referencing operator is not supported in PineForge.", "hint": null, "explanation": "TradingView refuses an assignment to a history reference; assign the variable itself.", "args": {"label": {"kind": "text"}}},
|
|
362
363
|
"PF-E6003": {"severity": "error", "area": "collection-history", "message": "{label} is a {kind}, which TradingView refuses for operator {op} (CE10123).", "hint": null, "explanation": "An array or matrix history read such as a[1] is an operand of an operator, which TradingView refuses; read an element or the size and use that instead.", "args": {"label": {"kind": "identifier"}, "kind": {"kind": "vocab", "values": ["array", "matrix"]}, "op": {"kind": "keyword"}}},
|
|
@@ -359,7 +359,7 @@ def no_data_request(node) -> str | None:
|
|
|
359
359
|
return None
|
|
360
360
|
|
|
361
361
|
|
|
362
|
-
def
|
|
362
|
+
def spell_call_spelling(node: FuncCall) -> str:
|
|
363
363
|
"""The call as a message names it: callee and its first arguments."""
|
|
364
364
|
ns, name = _call_name(node)
|
|
365
365
|
head = f"{ns}.{name}" if ns else str(name)
|
|
@@ -379,8 +379,13 @@ def spell_call(node: FuncCall) -> str:
|
|
|
379
379
|
shown = [arg(a) for a in node.args[:2]]
|
|
380
380
|
if len(node.args) > 2 or node.kwargs:
|
|
381
381
|
shown.append("...")
|
|
382
|
+
return f"{head}({', '.join(shown)})"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def spell_call(node: FuncCall) -> str:
|
|
386
|
+
"""The message's call spelling and its top-level source-line suffix."""
|
|
382
387
|
line = f" at line {node.loc.line}" if node.loc is not None else ""
|
|
383
|
-
return f"{
|
|
388
|
+
return f"{spell_call_spelling(node)}{line}"
|
|
384
389
|
|
|
385
390
|
|
|
386
391
|
class TradeSlice:
|
|
@@ -745,6 +750,21 @@ def unpinned_message(node: FuncCall) -> str:
|
|
|
745
750
|
return f"{spell_call(node)}: no data is pinned for this request, and its value was read"
|
|
746
751
|
|
|
747
752
|
|
|
753
|
+
def request_stop_marker(node: FuncCall) -> dict:
|
|
754
|
+
"""Keep stop provenance before lowering replaces a request's arguments."""
|
|
755
|
+
namespace, function = _call_name(node)
|
|
756
|
+
other_symbol = function in ("security", "security_lower_tf")
|
|
757
|
+
symbol = node.args[0] if node.args else node.kwargs.get("symbol")
|
|
758
|
+
return {
|
|
759
|
+
"message": unpinned_message(node),
|
|
760
|
+
"kind": "other_symbol" if other_symbol else "no_data",
|
|
761
|
+
"function": f"{namespace}.{function}",
|
|
762
|
+
"call": spell_call_spelling(node),
|
|
763
|
+
"line": node.loc.line if node.loc else 1,
|
|
764
|
+
"symbol_literal": symbol.value if isinstance(symbol, StringLiteral) else None,
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
|
|
748
768
|
def _na_of(request: FuncCall, funcs: dict[str, FuncDef]) -> ASTNode:
|
|
749
769
|
"""The ``na`` a request lowers to when no data is read for it."""
|
|
750
770
|
payload = None if no_data_request(request) else (
|
|
@@ -799,7 +819,7 @@ def lower_no_data_requests(program: Program) -> Program:
|
|
|
799
819
|
continue
|
|
800
820
|
if lowering == ABSENT_LOWERING:
|
|
801
821
|
_record_unpinned_capability(program, node)
|
|
802
|
-
node.annotations = {**node.annotations, UNPINNED_ANNOTATION:
|
|
822
|
+
node.annotations = {**node.annotations, UNPINNED_ANNOTATION: request_stop_marker(node)}
|
|
803
823
|
continue
|
|
804
824
|
swaps[id(node)] = _na_of(node, funcs)
|
|
805
825
|
if lowering == "unpinned":
|
|
@@ -811,12 +831,12 @@ def lower_no_data_requests(program: Program) -> Program:
|
|
|
811
831
|
if isinstance(stmt, (VarDecl, TupleAssign))}
|
|
812
832
|
for request in unpinned:
|
|
813
833
|
_mark_reads(program, index, declarations, request, swaps[id(request)],
|
|
814
|
-
|
|
834
|
+
request_stop_marker(request))
|
|
815
835
|
for request in backed:
|
|
816
836
|
ref = RequestRef()
|
|
817
837
|
request.annotations = {**(request.annotations or {}), REQUEST_REF_ANNOTATION: ref}
|
|
818
838
|
_mark_reads(program, index, declarations, request, request,
|
|
819
|
-
{
|
|
839
|
+
{**request_stop_marker(request), "ref": ref})
|
|
820
840
|
if request.annotations.get(LOWERING_ANNOTATION) == RECORDED_LOWERING:
|
|
821
841
|
# The key's other parts are constants: the call keeps its
|
|
822
842
|
# symbol, the one part the run computes.
|
|
@@ -872,7 +892,7 @@ def unpin_requests(program: Program, reasons: dict[int, str]) -> None:
|
|
|
872
892
|
marker = (request.annotations or {}).get(UNPINNED_ANNOTATION)
|
|
873
893
|
if isinstance(marker, dict):
|
|
874
894
|
lowered.annotations = {**(lowered.annotations or {}),
|
|
875
|
-
UNPINNED_ANNOTATION: marker
|
|
895
|
+
UNPINNED_ANNOTATION: {k: v for k, v in marker.items() if k != "ref"}}
|
|
876
896
|
swaps[request_id] = lowered
|
|
877
897
|
# The request leaves the program; its lowering is the deferred
|
|
878
898
|
# refusal's now (``request_discovery`` lists it so).
|
|
@@ -885,11 +905,12 @@ def unpin_requests(program: Program, reasons: dict[int, str]) -> None:
|
|
|
885
905
|
f"registration computes before the first bar; {reasons[request_id]}."))
|
|
886
906
|
for node in _nodes(program):
|
|
887
907
|
marker = (node.annotations or {}).get(UNPINNED_ANNOTATION)
|
|
888
|
-
if isinstance(marker, dict) and id(marker["ref"]) in chart_refs:
|
|
908
|
+
if isinstance(marker, dict) and "ref" in marker and id(marker["ref"]) in chart_refs:
|
|
889
909
|
node.annotations = {k: v for k, v in node.annotations.items()
|
|
890
910
|
if k != UNPINNED_ANNOTATION}
|
|
891
|
-
elif isinstance(marker, dict) and id(marker["ref"]) in refs:
|
|
892
|
-
node.annotations = {**node.annotations,
|
|
911
|
+
elif isinstance(marker, dict) and "ref" in marker and id(marker["ref"]) in refs:
|
|
912
|
+
node.annotations = {**node.annotations,
|
|
913
|
+
UNPINNED_ANNOTATION: {k: v for k, v in marker.items() if k != "ref"}}
|
|
893
914
|
if swaps:
|
|
894
915
|
replace_nodes(program, swaps)
|
|
895
916
|
notes = program.annotations = dict(program.annotations or {})
|
|
@@ -589,9 +589,14 @@ class Parser:
|
|
|
589
589
|
|
|
590
590
|
if (len(tokens) in (5, 7) and word(0) and word(2)
|
|
591
591
|
and values[1] == values[3] == "/"
|
|
592
|
-
and tokens[4].type == TokenType.NUMBER
|
|
592
|
+
and tokens[4].type == TokenType.NUMBER
|
|
593
|
+
and values[4].isascii() and values[4].isdecimal()
|
|
593
594
|
and (len(tokens) == 5 or (values[5] == "as" and word(6)))):
|
|
594
|
-
|
|
595
|
+
try:
|
|
596
|
+
version = int(values[4])
|
|
597
|
+
except ValueError:
|
|
598
|
+
return self._set_loc(node, start_tok)
|
|
599
|
+
node.user, node.name, node.version = values[0], values[2], version
|
|
595
600
|
node.path = f"{node.user}/{node.name}/{node.version}"
|
|
596
601
|
if len(tokens) == 7:
|
|
597
602
|
node.alias = values[6]
|
|
@@ -404,8 +404,8 @@ SYMINFO_FUNCTIONS: dict[str, IntrinsicFunc] = {}
|
|
|
404
404
|
def _syminfo_fn(short_name: str, *sigs: FuncSig) -> None:
|
|
405
405
|
SYMINFO_FUNCTIONS[short_name] = _func(f"syminfo.{short_name}", *sigs)
|
|
406
406
|
|
|
407
|
-
_syminfo_fn("prefix", _sig([], ret=S))
|
|
408
|
-
_syminfo_fn("ticker", _sig([], ret=S))
|
|
407
|
+
_syminfo_fn("prefix", _sig([("symbol", S)], ret=S))
|
|
408
|
+
_syminfo_fn("ticker", _sig([("symbol", S)], ret=S))
|
|
409
409
|
|
|
410
410
|
|
|
411
411
|
# ============================================================================
|