@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.
Files changed (34) hide show
  1. package/LICENSE +19 -3
  2. package/README.md +2 -1
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +6 -5
  5. package/pineforge_codegen/analyzer/base.py +3 -3
  6. package/pineforge_codegen/analyzer/types.py +7 -3
  7. package/pineforge_codegen/codegen/base.py +36 -17
  8. package/pineforge_codegen/codegen/capabilities.py +143 -10
  9. package/pineforge_codegen/codegen/checked_settings.py +161 -4
  10. package/pineforge_codegen/codegen/collection_history.py +163 -12
  11. package/pineforge_codegen/codegen/emit_top.py +29 -13
  12. package/pineforge_codegen/codegen/helpers.py +25 -1
  13. package/pineforge_codegen/codegen/host_members.py +1 -0
  14. package/pineforge_codegen/codegen/input.py +23 -8
  15. package/pineforge_codegen/codegen/order_shapes.py +356 -0
  16. package/pineforge_codegen/codegen/run_stops.py +52 -0
  17. package/pineforge_codegen/codegen/security.py +66 -10
  18. package/pineforge_codegen/codegen/tables.py +144 -28
  19. package/pineforge_codegen/codegen/tv_number_format.py +9 -2
  20. package/pineforge_codegen/codegen/types.py +85 -4
  21. package/pineforge_codegen/codegen/visit_call.py +235 -55
  22. package/pineforge_codegen/codegen/visit_expr.py +40 -8
  23. package/pineforge_codegen/codegen/visit_stmt.py +5 -4
  24. package/pineforge_codegen/collection_history.py +122 -20
  25. package/pineforge_codegen/diagnostics_catalog.json +1 -0
  26. package/pineforge_codegen/external_requests.py +30 -9
  27. package/pineforge_codegen/matrix_overloads.py +7 -0
  28. package/pineforge_codegen/parser.py +7 -2
  29. package/pineforge_codegen/signatures.py +2 -2
  30. package/pineforge_codegen/support_checker.py +101 -10
  31. package/pineforge_codegen-1.4.0.tar.gz +0 -0
  32. package/release.json +2 -2
  33. package/tables.json +7 -3
  34. 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
- return f"({''.join(arms)}({self._ternary_arm(links[-1].false_val, string_na)}))"
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
- if isinstance(marker, dict):
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'([&]() {{ pine_runtime_error(std::string("{self._cpp_string_escape(marker)}")); '
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
- return f"{self._collection_receiver_expr(name)}[{idx}]"
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
- lines.append(
1104
- f"{pad}/* drawing field assignment omitted: "
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
- pine_runtime_error("@HISTORICAL_CHANGE_MESSAGE@");
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
- pine_runtime_error(_PFCollectionTraits<T>::na_message());
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 spell_call(node: FuncCall) -> str:
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"{head}({', '.join(shown)}){line}"
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: unpinned_message(node)}
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
- unpinned_message(request))
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
- {"message": unpinned_message(request), "ref": ref})
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["message"]}
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, UNPINNED_ANNOTATION: marker["message"]}
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 {})
@@ -0,0 +1,7 @@
1
+ """Argument-dependent result types of numeric matrix operations."""
2
+
3
+ from .ast_nodes import FuncCall
4
+
5
+
6
+ def matrix_sum_has_rhs(call: FuncCall, *, namespace: bool = False) -> bool:
7
+ return len(call.args) > int(namespace) or "id2" in call.kwargs
@@ -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 and values[4].isdigit()
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
- node.user, node.name, node.version = values[0], values[2], int(values[4])
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
  # ============================================================================