@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
@@ -169,6 +169,7 @@ from .helpers import (
169
169
  from .tables import (
170
170
  ARRAY_DRAWING_NEW_CTORS,
171
171
  ARRAY_METHODS,
172
+ ARRAY_OPTIONAL_PARAMS,
172
173
  BAR_FIELDS,
173
174
  BAR_SERIES_PUSH,
174
175
  CHECKED_ARRAY_METHOD_KWARGS,
@@ -179,7 +180,10 @@ from .tables import (
179
180
  MATH_FUNC_MAP,
180
181
  MATRIX_METHODS,
181
182
  MATRIX_METHOD_KWARGS,
183
+ MATRIX_METHOD_KWARG_ALIASES,
182
184
  MATRIX_NUMERIC_ONLY,
185
+ MATRIX_OPTIONAL_PARAMS,
186
+ MATRIX_RECEIVER_KEYWORD,
183
187
  SKIP_FUNC_NAMES,
184
188
  SKIP_NAMESPACES,
185
189
  SKIP_VAR_TYPES,
@@ -188,6 +192,7 @@ from .tables import (
188
192
  TIME_FIELD_EXPRS,
189
193
  _math_minmax_na_expr,
190
194
  _math_round_digits_expr,
195
+ _matrix_functional_receiver,
191
196
  _merge_kwargs,
192
197
  )
193
198
 
@@ -690,10 +695,73 @@ class CallVisitor:
690
695
 
691
696
  def _array_method_arg_nodes(self, method: str, node: FuncCall) -> list:
692
697
  """Merge checked-array method kwargs into Pine signature order."""
693
- param_names = CHECKED_ARRAY_METHOD_KWARGS.get(method)
694
- if param_names is None:
698
+ return self._collection_arg_nodes("array", method, node, False)
699
+
700
+ def _collection_arg_nodes(self, namespace, method, node, functional):
701
+ """The arguments of a matrix or checked-array method call in Pine's
702
+ parameter order (the receiver first in the namespace form).
703
+
704
+ A keyword argument binds to its parameter's own slot, never shifted
705
+ left over a gap, and the gap before a later argument takes the
706
+ omitted optional parameter's default (a ``None`` default is the
707
+ receiver's own extent, which the method's template reads). A keyword
708
+ the lowering does not consume -- an unknown name, a parameter given
709
+ twice -- is refused, as is a gap before a required parameter. A call
710
+ without keywords, and an array method outside the checked-index
711
+ table, keep their arguments as written."""
712
+ if not node.kwargs:
695
713
  return list(node.args)
696
- return _merge_kwargs(node.args, node.kwargs, param_names, lambda arg: arg)
714
+ if namespace == "matrix":
715
+ slots = list(MATRIX_METHOD_KWARGS.get(method, []))
716
+ optional = MATRIX_OPTIONAL_PARAMS.get(method, {})
717
+ aliases = MATRIX_METHOD_KWARG_ALIASES.get(method, {})
718
+ receiver = MATRIX_RECEIVER_KEYWORD.get(method, "id")
719
+ elif method in CHECKED_ARRAY_METHOD_KWARGS:
720
+ slots = list(CHECKED_ARRAY_METHOD_KWARGS[method])
721
+ optional = ARRAY_OPTIONAL_PARAMS.get(method, {})
722
+ aliases = {}
723
+ receiver = "id"
724
+ else:
725
+ return list(node.args)
726
+ if functional:
727
+ slots.insert(0, receiver)
728
+ arguments = list(node.args)
729
+ for name, value in node.kwargs.items():
730
+ slot = aliases.get(name, name)
731
+ if slot not in slots:
732
+ self._collection_argument_error(node, namespace, method, "unknown", name)
733
+ index = slots.index(slot)
734
+ if index < len(arguments) and arguments[index] is not None:
735
+ self._collection_argument_error(node, namespace, method, "twice", name)
736
+ arguments.extend([None] * (index + 1 - len(arguments)))
737
+ arguments[index] = value
738
+ for index, argument in enumerate(arguments):
739
+ if argument is not None:
740
+ continue
741
+ if slots[index] not in optional:
742
+ self._collection_argument_error(node, namespace, method, "missing", slots[index])
743
+ default = optional[slots[index]]
744
+ arguments[index] = None if default is None else NumberLiteral(value=default)
745
+ return arguments
746
+
747
+ def _collection_argument_error(self, node, namespace, method, problem, name):
748
+ """Refuse a collection call's keyword binding with the diagnostic its
749
+ namespace already reports for a call that does not bind."""
750
+ if namespace == "matrix":
751
+ self._codegen_error(
752
+ node, f"matrix.{method}: wrong number of arguments",
753
+ hint="Check Pine v6 matrix method signature (positional vs keyword).")
754
+ key = ".".join((namespace, method))
755
+ if problem == "unknown":
756
+ self._codegen_error(node, f"{key}: unknown keyword argument '{name}'")
757
+ if problem == "twice":
758
+ self._codegen_error(
759
+ node, f"{key}: argument '{name}' passed both positionally and by keyword")
760
+ self._codegen_error(node, f"{key}: missing required argument '{name}'")
761
+
762
+ def _matrix_method_arg_nodes(self, method, node, functional=False):
763
+ arguments = self._collection_arg_nodes("matrix", method, node, functional)
764
+ return arguments[1:] if functional else arguments
697
765
 
698
766
  def _matrix_bool_value_args(
699
767
  self, method: str, args: list[str], arg_nodes: list,
@@ -740,12 +808,7 @@ class CallVisitor:
740
808
  "array.copy: expected exactly one receiver 'id'",
741
809
  hint="Use array.copy(source) or array.copy(id=source).",
742
810
  )
743
- param_names = CHECKED_ARRAY_METHOD_KWARGS.get(method)
744
- if param_names is None:
745
- return list(node.args)
746
- return _merge_kwargs(
747
- node.args, node.kwargs, ["id", *param_names], lambda arg: arg
748
- )
811
+ return self._collection_arg_nodes("array", method, node, True)
749
812
 
750
813
  def _map_identifier_is_visible_binding(self, node: FuncCall) -> bool:
751
814
  """Whether ``map`` is a lexical value at this exact source position.
@@ -1663,6 +1726,18 @@ class CallVisitor:
1663
1726
  return self._matrix_history_method_expr(
1664
1727
  callee.object, callee.member, recv_spec, node)
1665
1728
 
1729
+ if (isinstance(callee, MemberAccess)
1730
+ and isinstance(callee.object, (FuncCall, Ternary))
1731
+ and callee.member in MATRIX_METHODS):
1732
+ receiver_spec = self._type_spec_from_expr(callee.object)
1733
+ if receiver_spec is not None and receiver_spec.kind == "matrix":
1734
+ self._check_matrix_method_allowed(callee.member, receiver_spec, node)
1735
+ raw_args = self._matrix_method_arg_nodes(callee.member, node)
1736
+ arguments = self._matrix_bool_value_args(
1737
+ callee.member, [self._visit_expr(arg) if arg is not None else None for arg in raw_args], raw_args, receiver_spec)
1738
+ return self._matrix_method_expr(
1739
+ self._visit_expr(callee.object), callee.member, arguments, raw_args, node)
1740
+
1666
1741
  # chart.point.now/new/from_index/from_time/copy — REAL data (a ChartPoint
1667
1742
  # aggregate). Routed here BEFORE the obj.field.method receiver logic,
1668
1743
  # which would otherwise mis-treat ``chart.point`` as a receiver object
@@ -1821,20 +1896,16 @@ class CallVisitor:
1821
1896
  ):
1822
1897
  arr = self._collection_receiver_expr(oname)
1823
1898
  self._check_matrix_method_allowed(meth_raw, recv_spec, node)
1824
- param_names = MATRIX_METHOD_KWARGS.get(meth_raw)
1825
- raw_args = (
1826
- _merge_kwargs(node.args, node.kwargs, param_names, lambda a: a)
1827
- if param_names and node.kwargs else list(node.args)
1828
- )
1899
+ raw_args = self._matrix_method_arg_nodes(meth_raw, node)
1829
1900
  margs = self._matrix_bool_value_args(
1830
1901
  meth_raw,
1831
- [self._visit_expr(a) for a in raw_args],
1902
+ [self._visit_expr(a) if a is not None else None for a in raw_args],
1832
1903
  raw_args,
1833
1904
  recv_spec,
1834
1905
  )
1835
1906
  fn = MATRIX_METHODS[meth_raw]
1836
1907
  try:
1837
- return fn(arr, margs)
1908
+ return self._matrix_method_expr(arr, meth_raw, margs, raw_args, node)
1838
1909
  except IndexError:
1839
1910
  self._codegen_error(
1840
1911
  node,
@@ -2054,20 +2125,16 @@ class CallVisitor:
2054
2125
  ):
2055
2126
  arr = self._collection_receiver_expr(namespace)
2056
2127
  self._check_matrix_method_allowed(func_name, namespace_spec, node)
2057
- param_names = MATRIX_METHOD_KWARGS.get(func_name)
2058
- raw_args = (
2059
- _merge_kwargs(node.args, node.kwargs, param_names, lambda a: a)
2060
- if param_names and node.kwargs else list(node.args)
2061
- )
2128
+ raw_args = self._matrix_method_arg_nodes(func_name, node)
2062
2129
  args = self._matrix_bool_value_args(
2063
2130
  func_name,
2064
- [self._visit_expr(a) for a in raw_args],
2131
+ [self._visit_expr(a) if a is not None else None for a in raw_args],
2065
2132
  raw_args,
2066
2133
  namespace_spec,
2067
2134
  )
2068
2135
  fn = MATRIX_METHODS[func_name]
2069
2136
  try:
2070
- return fn(arr, args)
2137
+ return self._matrix_method_expr(arr, func_name, args, raw_args, node)
2071
2138
  except IndexError:
2072
2139
  self._codegen_error(
2073
2140
  node,
@@ -2108,7 +2175,22 @@ class CallVisitor:
2108
2175
  init_val = self._array_init_value_expr(elem_spec, node.args[1])
2109
2176
  else:
2110
2177
  init_val = init_default
2111
- return f"{cpp_type}((size_t)({size_arg}), {init_val})"
2178
+ return (
2179
+ "[&](auto _pf_array_new_size, auto _pf_array_new_value){ "
2180
+ "using _pf_array_new_size_type=decltype(_pf_array_new_size); "
2181
+ "if constexpr(!std::is_same_v<_pf_array_new_size_type,bool>) { "
2182
+ "if(is_na(_pf_array_new_size)) "
2183
+ '_PF_ARRAY_STOP("size_invalid", "new", "cannot create std::vector larger than max_size()"); '
2184
+ "} if(!std::isfinite((double)_pf_array_new_size)||"
2185
+ "_pf_array_new_size<0||"
2186
+ "(long double)_pf_array_new_size>(long double)std::numeric_limits<size_t>::max()) "
2187
+ '_PF_ARRAY_STOP("size_invalid", "new", "cannot create std::vector larger than max_size()"); '
2188
+ f"try {{ return {cpp_type}((size_t)(_pf_array_new_size), _pf_array_new_value); "
2189
+ "} catch (const std::length_error& _pf_array_new_error) { "
2190
+ '_PF_ARRAY_STOP("size_invalid", "new", _pf_array_new_error.what()); '
2191
+ f"return {cpp_type}(); }} "
2192
+ f"}}(({size_arg}), ({init_val}))"
2193
+ )
2112
2194
  return f"{cpp_type}()"
2113
2195
  if func_name == "from":
2114
2196
  spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float"))
@@ -2282,8 +2364,8 @@ class CallVisitor:
2282
2364
  # runtime.error() and other runtime.* calls
2283
2365
  if namespace == "runtime":
2284
2366
  if func_name == "error":
2285
- rt_args = [self._visit_expr(a) for a in node.args]
2286
- msg_arg = rt_args[0] if rt_args else '""'
2367
+ message = node.args[0] if node.args else node.kwargs.get("message")
2368
+ msg_arg = self._visit_expr(message) if message is not None else '""'
2287
2369
  return f'pine_runtime_error({msg_arg})'
2288
2370
  return '"" /* unsupported runtime */'
2289
2371
 
@@ -2615,10 +2697,31 @@ class CallVisitor:
2615
2697
  return f"na<double>() /* unsupported: ta.{func_name} */"
2616
2698
 
2617
2699
  if namespace == "syminfo":
2618
- if func_name == "prefix":
2619
- return "_pf_derive_prefix(syminfo_.tickerid)"
2620
- if func_name == "ticker":
2621
- return "syminfo_.ticker"
2700
+ if func_name in {"prefix", "ticker"}:
2701
+ symbol = node.args[0] if node.args else node.kwargs.get("symbol")
2702
+ symbol_expr = self._visit_expr(symbol) if symbol is not None else "syminfo_.tickerid"
2703
+ if symbol_expr == "syminfo_.tickerid":
2704
+ return ("_pf_derive_prefix(syminfo_.tickerid)" if func_name == "prefix"
2705
+ else "syminfo_.ticker")
2706
+ if func_name == "prefix":
2707
+ result_expr = (
2708
+ "_pf_colon == std::string::npos "
2709
+ "|| _pf_colon + 1 == _pf_symbol.size() "
2710
+ "? std::string() : _pf_symbol.substr(0, _pf_colon)"
2711
+ )
2712
+ else:
2713
+ result_expr = (
2714
+ "_pf_colon == std::string::npos "
2715
+ "? _pf_symbol : _pf_symbol.substr(_pf_colon + 1)"
2716
+ )
2717
+ return (
2718
+ "([](const auto& _pf_symbol) { "
2719
+ "if constexpr (std::is_same_v<std::decay_t<decltype(_pf_symbol)>, std::string>) { "
2720
+ "const auto _pf_colon = _pf_symbol.find(':'); "
2721
+ f"return {result_expr}; "
2722
+ "} else { return std::string(); } "
2723
+ f"}})({symbol_expr})"
2724
+ )
2622
2725
  return f"na<double>() /* unsupported: syminfo.{func_name} */"
2623
2726
 
2624
2727
  # str.* fallback now handled by _visit_str_call above
@@ -2643,41 +2746,48 @@ class CallVisitor:
2643
2746
  if len(args_e) > 2 else self._default_for_spec(elem_spec)
2644
2747
  )
2645
2748
  return f"PineGenericMatrix<{cpp_t}>::new_({rows}, {cols}, {init})"
2646
- if func_name in MATRIX_METHODS and node.args:
2749
+ matrix_receiver = _matrix_functional_receiver(func_name, node)
2750
+ if func_name in MATRIX_METHODS and matrix_receiver is None:
2751
+ self._codegen_error(
2752
+ node, f"matrix.{func_name}: wrong number of arguments",
2753
+ hint="Check Pine v6 matrix method signature (positional vs keyword).")
2754
+ if func_name in MATRIX_METHODS and matrix_receiver is not None:
2647
2755
  from ..ast_nodes import Identifier as _Ident
2648
2756
  if func_name in MATRIX_NUMERIC_ONLY:
2649
- recv_node = node.args[0]
2757
+ recv_node = matrix_receiver
2650
2758
  if history_annotation(recv_node) is not None:
2651
2759
  # A matrix's history is a matrix of its type.
2652
2760
  recv_node = recv_node.object
2653
- if not isinstance(recv_node, _Ident):
2654
- self._codegen_error(node, f"matrix.{func_name} receiver must be a variable reference")
2655
- recv_name = recv_node.name
2656
- recv_spec = self._collection_spec_for_name(recv_name)
2657
- if recv_spec is None or recv_spec.kind != "matrix":
2658
- self._codegen_error(node, f"matrix.{func_name}: receiver '{recv_name}' is not a known matrix variable")
2761
+ if func_name == "sum":
2762
+ recv_spec = self._type_spec_from_expr(recv_node)
2763
+ if recv_spec is None or recv_spec.kind != "matrix":
2764
+ name = func_name
2765
+ self._codegen_error(node, f"matrix.{name}(...) is not implemented in PineForge runtime.")
2766
+ else:
2767
+ if not isinstance(recv_node, _Ident):
2768
+ self._codegen_error(node, f"matrix.{func_name} receiver must be a variable reference")
2769
+ recv_name = recv_node.name
2770
+ recv_spec = self._collection_spec_for_name(recv_name)
2771
+ if recv_spec is None or recv_spec.kind != "matrix":
2772
+ self._codegen_error(node, f"matrix.{func_name}: receiver '{recv_name}' is not a known matrix variable")
2659
2773
  self._check_matrix_method_allowed(func_name, recv_spec, node)
2660
2774
  if func_name == "sort":
2661
- if isinstance(node.args[0], _Ident):
2662
- recv_name = node.args[0].name
2775
+ if isinstance(matrix_receiver, _Ident):
2776
+ recv_name = matrix_receiver.name
2663
2777
  recv_spec = self._collection_spec_for_name(recv_name)
2664
2778
  if recv_spec is not None and recv_spec.kind == "matrix":
2665
2779
  self._check_matrix_method_allowed(func_name, recv_spec, node)
2666
- obj = self._visit_expr(node.args[0])
2667
- param_names = MATRIX_METHOD_KWARGS.get(func_name)
2668
- raw_rest = (
2669
- _merge_kwargs(node.args[1:], node.kwargs, param_names, lambda a: a)
2670
- if param_names else list(node.args[1:])
2671
- )
2780
+ obj = self._visit_expr(matrix_receiver)
2781
+ raw_rest = self._matrix_method_arg_nodes(func_name, node, True)
2672
2782
  rest = self._matrix_bool_value_args(
2673
2783
  func_name,
2674
- [self._visit_expr(a) for a in raw_rest],
2784
+ [self._visit_expr(a) if a is not None else None for a in raw_rest],
2675
2785
  raw_rest,
2676
- self._type_spec_from_expr(node.args[0]),
2786
+ self._type_spec_from_expr(matrix_receiver),
2677
2787
  )
2678
2788
  fn = MATRIX_METHODS[func_name]
2679
2789
  try:
2680
- return fn(obj, rest)
2790
+ return self._matrix_method_expr(obj, func_name, rest, raw_rest, node)
2681
2791
  except IndexError:
2682
2792
  self._codegen_error(
2683
2793
  node,
@@ -3631,13 +3741,22 @@ class CallVisitor:
3631
3741
  return 'std::string("")'
3632
3742
 
3633
3743
  if func_name == "substring":
3634
- if len(args) == 3:
3635
- # begin_pos is read twice: evaluate it once.
3744
+ if len(args) in (2, 3):
3745
+ end_parameter = ", auto _pf_substring_end" if len(args) == 3 else ""
3746
+ count = ", _pf_substring_end - _pf_substring_begin" if len(args) == 3 else ""
3747
+
3748
+ def checked_substring(bound):
3749
+ return (
3750
+ f"[&](const std::string& _pf_substring_text, auto _pf_substring_begin{end_parameter}){{ "
3751
+ "try { return _pf_substring_text.substr(_pf_substring_begin"
3752
+ f"{count}); }} catch (const std::out_of_range& _pf_substring_error) {{ "
3753
+ '_PF_STRING_STOP("substring_out_of_range", _pf_substring_error.what()); '
3754
+ "return std::string(); } "
3755
+ "}(" + ", ".join(f"({arg})" for arg in bound) + ")"
3756
+ )
3757
+
3636
3758
  return evaluate_args_once(
3637
- args, (1,), lambda a: f"{a[0]}.substr({a[1]}, {a[2]} - {a[1]})",
3638
- "_pf_str_a")
3639
- elif len(args) == 2:
3640
- return f"{args[0]}.substr({args[1]})"
3759
+ args, range(len(args)), checked_substring, "_pf_substring_arg")
3641
3760
  return 'std::string("")'
3642
3761
 
3643
3762
  if func_name == "format":
@@ -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:
@@ -1960,7 +1974,13 @@ class ExprVisitor:
1960
1974
  "collection IDs, so this result can differ from TradingView "
1961
1975
  "and a missing index is not represented faithfully.",
1962
1976
  )
1963
- 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}]"
1964
1984
  # Handle strategy.* history access (e.g., strategy.position_size[1])
1965
1985
  if isinstance(node.object, MemberAccess):
1966
1986
  if isinstance(node.object.object, Identifier):
@@ -2100,4 +2120,10 @@ class ExprVisitor:
2100
2120
  self._warn_untracked_reference_history(node)
2101
2121
  return obj
2102
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
+ )
2103
2129
  return f"{obj}[{idx_int}]"
@@ -1822,6 +1822,9 @@ class StmtVisitor:
1822
1822
  self._current_counted_loop_vars = (
1823
1823
  self._current_counted_loop_vars - {node.var, *(node.vars or ())})
1824
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})"
1825
1828
  elem_spec = (
1826
1829
  iterable_spec.element
1827
1830
  if iterable_spec is not None and iterable_spec.kind == "array"