@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.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/pineforge_codegen/__init__.py +6 -5
  3. package/pineforge_codegen/analyzer/base.py +3 -3
  4. package/pineforge_codegen/analyzer/types.py +7 -3
  5. package/pineforge_codegen/codegen/base.py +26 -16
  6. package/pineforge_codegen/codegen/checked_settings.py +113 -2
  7. package/pineforge_codegen/codegen/collection_history.py +163 -12
  8. package/pineforge_codegen/codegen/emit_top.py +24 -8
  9. package/pineforge_codegen/codegen/helpers.py +19 -1
  10. package/pineforge_codegen/codegen/host_members.py +1 -0
  11. package/pineforge_codegen/codegen/input.py +23 -8
  12. package/pineforge_codegen/codegen/run_stops.py +52 -0
  13. package/pineforge_codegen/codegen/security.py +12 -5
  14. package/pineforge_codegen/codegen/tables.py +132 -28
  15. package/pineforge_codegen/codegen/tv_number_format.py +9 -2
  16. package/pineforge_codegen/codegen/types.py +85 -4
  17. package/pineforge_codegen/codegen/visit_call.py +174 -55
  18. package/pineforge_codegen/codegen/visit_expr.py +32 -6
  19. package/pineforge_codegen/codegen/visit_stmt.py +3 -0
  20. package/pineforge_codegen/collection_history.py +122 -20
  21. package/pineforge_codegen/diagnostics_catalog.json +1 -0
  22. package/pineforge_codegen/external_requests.py +30 -9
  23. package/pineforge_codegen/matrix_overloads.py +7 -0
  24. package/pineforge_codegen/parser.py +7 -2
  25. package/pineforge_codegen/signatures.py +2 -2
  26. package/pineforge_codegen/support_checker.py +82 -0
  27. package/pineforge_codegen-1.4.0.tar.gz +0 -0
  28. package/release.json +2 -2
  29. package/tables.json +7 -3
  30. 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
- 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
  # ============================================================================
@@ -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.3.0",
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": "b058ef7c8ded42e7a80ccf551e62985e052d1e306aebf615ab9937544f060839"
6
+ "sha256": "fe674a25ca188d1d6902b01f81d040fc62e6ad9ee325afae7287d990248e1f97"
7
7
  }
package/tables.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "CODEGEN_VERSION": "1.3.0",
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
- "syminfo.ticker": [],
1124
+ "syminfo.prefix": [
1125
+ "symbol"
1126
+ ],
1127
+ "syminfo.ticker": [
1128
+ "symbol"
1129
+ ],
1126
1130
  "na": [
1127
1131
  "x"
1128
1132
  ],
Binary file