@pineforge/codegen-pyodide 1.3.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/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 +26 -16
- package/pineforge_codegen/codegen/checked_settings.py +113 -2
- package/pineforge_codegen/codegen/collection_history.py +163 -12
- package/pineforge_codegen/codegen/emit_top.py +24 -8
- package/pineforge_codegen/codegen/helpers.py +19 -1
- package/pineforge_codegen/codegen/host_members.py +1 -0
- package/pineforge_codegen/codegen/input.py +23 -8
- package/pineforge_codegen/codegen/run_stops.py +52 -0
- package/pineforge_codegen/codegen/security.py +12 -5
- package/pineforge_codegen/codegen/tables.py +132 -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 +174 -55
- package/pineforge_codegen/codegen/visit_expr.py +32 -6
- package/pineforge_codegen/codegen/visit_stmt.py +3 -0
- 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 +82 -0
- package/pineforge_codegen-1.4.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -3
- package/pineforge_codegen-1.3.0.tar.gz +0 -0
|
@@ -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
|
# ============================================================================
|
|
@@ -549,6 +549,20 @@ def _qualified_name(callee: ASTNode) -> tuple[str | None, str | None]:
|
|
|
549
549
|
return None, None
|
|
550
550
|
|
|
551
551
|
|
|
552
|
+
# The ``array.*`` functions whose value is a new array.
|
|
553
|
+
_ARRAY_VALUED_FUNCTIONS = frozenset({"new", "from", "copy", "slice"})
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _is_array_valued_call(node) -> bool:
|
|
557
|
+
"""Whether ``node`` is a call of ``array.new``, ``array.new_<type>``,
|
|
558
|
+
``array.from``, ``array.copy`` or ``array.slice``."""
|
|
559
|
+
if not isinstance(node, FuncCall):
|
|
560
|
+
return False
|
|
561
|
+
namespace, function = _qualified_name(node.callee)
|
|
562
|
+
return namespace == "array" and function is not None and (
|
|
563
|
+
function in _ARRAY_VALUED_FUNCTIONS or function.startswith("new_"))
|
|
564
|
+
|
|
565
|
+
|
|
552
566
|
def _resolve_member_chain(node: ASTNode) -> str | None:
|
|
553
567
|
"""Flatten a MemberAccess/Identifier chain into a dotted name."""
|
|
554
568
|
if isinstance(node, Identifier):
|
|
@@ -661,6 +675,11 @@ class SupportChecker:
|
|
|
661
675
|
# then ``request.security(haTicker, ...)``, or ``reqSym = cond ? other :
|
|
662
676
|
# syminfo.tickerid``. First binding wins (closest to declaration).
|
|
663
677
|
self._scalar_defs: dict[str, ASTNode] = {}
|
|
678
|
+
# Variables declared an array (by an array-valued call or an
|
|
679
|
+
# ``array<...>`` type), and the names the script binds exactly once:
|
|
680
|
+
# the receivers ``_check_bound_array_concat`` resolves by name.
|
|
681
|
+
self._array_names: set[str] = set()
|
|
682
|
+
self._bound_once: set[str] | None = None
|
|
664
683
|
# EVERY reassignment value bound to a scalar name (``name := <expr>``
|
|
665
684
|
# and the compound forms). A declaration alone does not pin the value a
|
|
666
685
|
# ``request.security`` symbol argument actually carries: the engine is
|
|
@@ -1335,6 +1354,10 @@ class SupportChecker:
|
|
|
1335
1354
|
|
|
1336
1355
|
def _visit_VarDecl(self, node: VarDecl) -> None:
|
|
1337
1356
|
self._check_tuple_literal_value(node.value)
|
|
1357
|
+
self._check_bound_array_concat(node.value)
|
|
1358
|
+
if node.name and (_is_array_valued_call(node.value)
|
|
1359
|
+
or (node.type_hint or "").startswith("array<")):
|
|
1360
|
+
self._array_names.add(node.name)
|
|
1338
1361
|
if node.name and node.value is not None:
|
|
1339
1362
|
self._scalar_defs.setdefault(node.name, node.value)
|
|
1340
1363
|
if node.name and (node.is_var or node.is_varip):
|
|
@@ -1367,6 +1390,7 @@ class SupportChecker:
|
|
|
1367
1390
|
self._visit_children(node)
|
|
1368
1391
|
|
|
1369
1392
|
def _visit_Assignment(self, node: Assignment) -> None:
|
|
1393
|
+
self._check_bound_array_concat(node.value)
|
|
1370
1394
|
if isinstance(node.target, TupleLiteral):
|
|
1371
1395
|
# ``[p, q] := f()`` reached the C++ as an assignment to a
|
|
1372
1396
|
# temporary tuple, a silent no-op. TradingView rejects it
|
|
@@ -1390,6 +1414,64 @@ class SupportChecker:
|
|
|
1390
1414
|
self._record_scalar_rebind(node)
|
|
1391
1415
|
self._visit_children(node)
|
|
1392
1416
|
|
|
1417
|
+
def _check_bound_array_concat(self, value) -> None:
|
|
1418
|
+
"""Refuse the value of ``array.concat``: PineForge lowers the call as
|
|
1419
|
+
the statement that appends to the first array (``a.concat(b)`` or
|
|
1420
|
+
``array.concat(a, b)``), which has no value to bind. The namespace
|
|
1421
|
+
call is refused wherever its value is bound; the method form only on a
|
|
1422
|
+
receiver known to be an array (a call that builds one, or a variable
|
|
1423
|
+
declared as one that the script binds exactly once, so that a
|
|
1424
|
+
parameter or a local of another type never shares the name), and never
|
|
1425
|
+
when the script defines a method of that name."""
|
|
1426
|
+
if not isinstance(value, FuncCall):
|
|
1427
|
+
return
|
|
1428
|
+
if isinstance(value.callee, MemberAccess):
|
|
1429
|
+
# ``_qualified_name`` names no call receiver (``array.copy(a).concat(b)``).
|
|
1430
|
+
method = value.callee.member
|
|
1431
|
+
namespace = (value.callee.object.name
|
|
1432
|
+
if isinstance(value.callee.object, Identifier) else None)
|
|
1433
|
+
else:
|
|
1434
|
+
namespace, method = _qualified_name(value.callee)
|
|
1435
|
+
if method != "concat":
|
|
1436
|
+
return
|
|
1437
|
+
if namespace != "array":
|
|
1438
|
+
if method in self._user_methods or not isinstance(value.callee, MemberAccess):
|
|
1439
|
+
return
|
|
1440
|
+
receiver = value.callee.object
|
|
1441
|
+
while isinstance(receiver, Subscript):
|
|
1442
|
+
receiver = receiver.object
|
|
1443
|
+
if not (_is_array_valued_call(receiver)
|
|
1444
|
+
or (isinstance(receiver, Identifier)
|
|
1445
|
+
and receiver.name in self._array_names
|
|
1446
|
+
and receiver.name in self._names_bound_once())):
|
|
1447
|
+
return
|
|
1448
|
+
name = method
|
|
1449
|
+
self._err(value, f"array.{name}(...) is not implemented in PineForge runtime.")
|
|
1450
|
+
|
|
1451
|
+
def _names_bound_once(self) -> set[str]:
|
|
1452
|
+
"""The names the script binds exactly once: by one declaration, with
|
|
1453
|
+
no function or method parameter, loop variable, tuple element or
|
|
1454
|
+
second declaration (a local beside a global) sharing the name."""
|
|
1455
|
+
if self._bound_once is None:
|
|
1456
|
+
counts: dict[str, int] = {}
|
|
1457
|
+
for item in _walk_nodes(self._ast):
|
|
1458
|
+
if isinstance(item, VarDecl):
|
|
1459
|
+
names = (item.name,)
|
|
1460
|
+
elif isinstance(item, TupleAssign):
|
|
1461
|
+
names = tuple(item.names)
|
|
1462
|
+
elif isinstance(item, (FuncDef, MethodDef)):
|
|
1463
|
+
names = tuple(getattr(param, "name", param) for param in item.params)
|
|
1464
|
+
elif isinstance(item, ForStmt):
|
|
1465
|
+
names = (item.var,)
|
|
1466
|
+
elif isinstance(item, ForInStmt):
|
|
1467
|
+
names = tuple(item.vars or ()) + ((item.var,) if item.var else ())
|
|
1468
|
+
else:
|
|
1469
|
+
continue
|
|
1470
|
+
for name in names:
|
|
1471
|
+
counts[name] = counts.get(name, 0) + 1
|
|
1472
|
+
self._bound_once = {name for name, count in counts.items() if count == 1}
|
|
1473
|
+
return self._bound_once
|
|
1474
|
+
|
|
1393
1475
|
def _check_tuple_literal_value(self, value) -> None:
|
|
1394
1476
|
"""Refuse a tuple literal or a ternary of tuples as a declaration's
|
|
1395
1477
|
or reassignment's value.
|
|
Binary file
|
package/release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codegen": "1.
|
|
2
|
+
"codegen": "1.4.0",
|
|
3
3
|
"pyodide": "314.0.0",
|
|
4
4
|
"python": "3.14.0",
|
|
5
5
|
"emscripten": "emscripten_5_0_3",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "fe674a25ca188d1d6902b01f81d040fc62e6ad9ee325afae7287d990248e1f97"
|
|
7
7
|
}
|
package/tables.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"CODEGEN_VERSION": "1.
|
|
2
|
+
"CODEGEN_VERSION": "1.4.0",
|
|
3
3
|
"TA_CLASS_MAP_KEYS": [
|
|
4
4
|
"sma",
|
|
5
5
|
"ema",
|
|
@@ -1121,8 +1121,12 @@
|
|
|
1121
1121
|
"id",
|
|
1122
1122
|
"id2"
|
|
1123
1123
|
],
|
|
1124
|
-
"syminfo.prefix": [
|
|
1125
|
-
|
|
1124
|
+
"syminfo.prefix": [
|
|
1125
|
+
"symbol"
|
|
1126
|
+
],
|
|
1127
|
+
"syminfo.ticker": [
|
|
1128
|
+
"symbol"
|
|
1129
|
+
],
|
|
1126
1130
|
"na": [
|
|
1127
1131
|
"x"
|
|
1128
1132
|
],
|
|
Binary file
|