@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
@@ -46,6 +46,7 @@ from ..collection_history import history_annotation
46
46
  from ..errors import Phase
47
47
  from ..external_requests import UNPINNED_ANNOTATION
48
48
  from ..limits import iter_ast_nodes
49
+ from ..matrix_overloads import matrix_sum_has_rhs
49
50
  from ..symbols import PineType, TypeSpec, method_receiver_type_name
50
51
  from .helpers import (
51
52
  NA_PRESERVING_INT_TYPES,
@@ -59,6 +60,7 @@ from .tables import (
59
60
  ARRAY_ARGS_READ_REPEATEDLY,
60
61
  ARRAY_DRAWING_NEW_CTORS,
61
62
  ARRAY_METHODS,
63
+ MATRIX_METHODS,
62
64
  V5_ARRAY_INDEX_METHODS,
63
65
  BAR_BUILTINS,
64
66
  BAR_FIELDS,
@@ -68,6 +70,7 @@ from .tables import (
68
70
  MATRIX_RETURNING_METHODS,
69
71
  PINE_TYPE_TO_CPP,
70
72
  TA_RETURNS_BOOL,
73
+ _matrix_functional_receiver,
71
74
  checked_array_slice,
72
75
  )
73
76
 
@@ -1776,8 +1779,10 @@ class TypeInferer:
1776
1779
  if func_name in ("copy", "slice"):
1777
1780
  return arg_spec
1778
1781
  return arg_spec.element
1779
- if namespace == "matrix" and func_name in MATRIX_RETURNING_METHODS:
1780
- receiver_node = node.args[0] if node.args else node.kwargs.get("id")
1782
+ if namespace == "matrix" and (func_name in MATRIX_RETURNING_METHODS or (
1783
+ func_name == "sum" and matrix_sum_has_rhs(node, namespace=True))):
1784
+ receiver_node = (node.args[0] if node.args else
1785
+ node.kwargs.get("id1", node.kwargs.get("id")))
1781
1786
  receiver_spec = self._type_spec_from_expr(receiver_node)
1782
1787
  if receiver_spec is not None and receiver_spec.kind == "matrix":
1783
1788
  return receiver_spec
@@ -1869,7 +1874,8 @@ class TypeInferer:
1869
1874
  method = (member_name
1870
1875
  if history_annotation(node.callee.object) is not None
1871
1876
  else func_name)
1872
- if method in MATRIX_RETURNING_METHODS:
1877
+ if method in MATRIX_RETURNING_METHODS or (
1878
+ method == "sum" and matrix_sum_has_rhs(node)):
1873
1879
  return recv_spec
1874
1880
  if method in ("row", "col"):
1875
1881
  return TypeSpec.array(recv_spec.element)
@@ -1961,6 +1967,70 @@ class TypeInferer:
1961
1967
  # Method lowering for collection types (used by visit_call paths)
1962
1968
  # ------------------------------------------------------------------
1963
1969
 
1970
+ def _matrix_method_expr(self, receiver, method, arguments, argument_nodes, node):
1971
+ functional = (isinstance(node.callee, MemberAccess)
1972
+ and isinstance(node.callee.object, Identifier)
1973
+ and node.callee.object.name == "matrix")
1974
+ receiver_node = (_matrix_functional_receiver(method, node)
1975
+ if functional else node.callee.object)
1976
+ receiver = self._checked_matrix_history_receiver(receiver, receiver_node)
1977
+ if method in ("add_row", "add_col") and len(arguments) == 2 and arguments[0] is None:
1978
+ # Only ``array_id`` is written (``m.add_row(array_id = a)``): the
1979
+ # call is the array-only form, which appends it.
1980
+ arguments = [arguments[1]]
1981
+ argument_nodes = [argument_nodes[1]]
1982
+ if method in ("add_row", "add_col") and len(arguments) < 2:
1983
+ argument_spec = (self._type_spec_from_expr(argument_nodes[0])
1984
+ if argument_nodes else None)
1985
+ # An index-only call: the argument is a number whose spec the
1986
+ # analysis knows, or one it cannot type that reads as a number
1987
+ # (a call, an input, ``bar_index % 2``): the generated lambda then
1988
+ # tells an index from an array by the argument's C++ type.
1989
+ typed_index = not arguments or (
1990
+ argument_spec is not None and argument_spec.kind == "primitive")
1991
+ untyped_number = (argument_spec is None and bool(arguments)
1992
+ and self._infer_type(argument_nodes[0]) in ("double", "int", "int64_t"))
1993
+ receiver_spec = self._type_spec_from_expr(receiver_node)
1994
+ if ((typed_index or untyped_number) and receiver_spec is not None
1995
+ and receiver_spec.element is not None):
1996
+ element_spec = receiver_spec.element
1997
+ element_cpp = self._type_spec_to_cpp(element_spec)
1998
+ count = "columns" if method == "add_row" else "rows"
1999
+ append = "rows" if method == "add_row" else "columns"
2000
+ values = (f"std::vector<{element_cpp}>((size_t)_pf_matrix_target.{count}(), "
2001
+ f"{self._array_init_value_expr(element_spec, NaLiteral())})")
2002
+ if untyped_number:
2003
+ inserted = MATRIX_METHODS[method]("_pf_matrix_target", ["_pf_matrix_arg", values])
2004
+ appended = MATRIX_METHODS[method]("_pf_matrix_target", ["_pf_matrix_arg"])
2005
+ return (
2006
+ "[&](auto&& _pf_matrix_target, auto&& _pf_matrix_arg) { "
2007
+ "if constexpr (std::is_arithmetic_v<std::decay_t<decltype(_pf_matrix_arg)>>) { "
2008
+ f"{inserted}; }} else {{ {appended}; }} }}({receiver}, {arguments[0]})")
2009
+ index = arguments[0] if arguments else f"_pf_matrix_target.{append}()"
2010
+ mutation = MATRIX_METHODS[method]("_pf_matrix_target", [index, values])
2011
+ return f"[&](auto&& _pf_matrix_target) {{ {mutation}; }}({receiver})"
2012
+ if method in ("sum", "diff", "mult") and argument_nodes:
2013
+ other_spec = self._type_spec_from_expr(argument_nodes[0])
2014
+ if (method == "mult" and other_spec is not None
2015
+ and other_spec.kind == "array"):
2016
+ name = method
2017
+ self._codegen_error(
2018
+ node, f"matrix.{name}(...) is not implemented in PineForge runtime.")
2019
+ numeric_scalar = ((other_spec is not None and other_spec.kind == "primitive"
2020
+ and other_spec.name in ("int", "float"))
2021
+ or (other_spec is None and self._infer_type(argument_nodes[0])
2022
+ in ("double", "int", "int64_t")))
2023
+ if method in ("diff", "mult") and len(arguments) == 1 and numeric_scalar:
2024
+ operation = "-=" if method == "diff" else "*="
2025
+ return (
2026
+ "([](const auto& _pf_matrix_left, const auto _pf_matrix_right) { "
2027
+ "auto _pf_matrix_result = _pf_matrix_left.copy(); "
2028
+ f"_pf_matrix_result.data().array() {operation} _pf_matrix_right; "
2029
+ "return _pf_matrix_result; "
2030
+ f"}}({receiver}, {arguments[0]}))"
2031
+ )
2032
+ return MATRIX_METHODS[method](receiver, arguments)
2033
+
1964
2034
  def _array_receiver_once_expr(
1965
2035
  self, array_expr: str, args: list[str], lower_receiver,
1966
2036
  ) -> str:
@@ -2004,6 +2074,10 @@ class TypeInferer:
2004
2074
  spec: TypeSpec | None = None, node: ASTNode | None = None,
2005
2075
  ) -> str:
2006
2076
  """Lower ``arr.method(...)`` to its C++ form, validating numeric requirements."""
2077
+ if self._array_history_value_names():
2078
+ array_expr = f"_pf_array_id({array_expr})"
2079
+ if method in ("covariance", "concat") and args:
2080
+ args = [f"_pf_array_id({args[0]})", *args[1:]]
2007
2081
  spec = spec or TypeSpec.array(TypeSpec.primitive("float"))
2008
2082
  arr_cpp_type = self._type_spec_to_cpp(spec)
2009
2083
  elem_cpp = self._type_spec_to_cpp(spec.element) if spec.element is not None else "double"
@@ -2103,7 +2177,10 @@ class TypeInferer:
2103
2177
  )
2104
2178
  return lowered
2105
2179
 
2106
- return self._array_receiver_once_expr(array_expr, args, lower_receiver)
2180
+ try:
2181
+ return self._array_receiver_once_expr(array_expr, args, lower_receiver)
2182
+ except IndexError:
2183
+ self._codegen_error(node, f"array.{method}: wrong number of arguments")
2107
2184
 
2108
2185
  def _map_method_expr(
2109
2186
  self, map_expr: str, method: str, args: list[str], spec: TypeSpec | None = None,
@@ -2150,6 +2227,10 @@ class TypeInferer:
2150
2227
 
2151
2228
  def _type_for_decl(self, node: VarDecl) -> str:
2152
2229
  """Determine the C++ type for a ``VarDecl``: explicit hint, then symbol, then RHS inference."""
2230
+ history_value_cpp = (self._array_history_value_cpp_type(node.name)
2231
+ if not getattr(self, "_active_func_name", None) else None)
2232
+ if history_value_cpp is not None:
2233
+ return history_value_cpp
2153
2234
  def promote_wide_int(cpp_type: str) -> str:
2154
2235
  if cpp_type != "int":
2155
2236
  return cpp_type
@@ -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
 
@@ -432,6 +437,7 @@ class CallVisitor:
432
437
 
433
438
  close = "true" if func_name == "time_close" else "false"
434
439
  empty = 'std::string("")'
440
+ self._order_shape_host_reads.add("pine_time_offset")
435
441
  return (
436
442
  f"pine_time_offset(current_bar_.timestamp, {offset('bars_back')}, "
437
443
  f"{text('timeframe', 'script_tf_')}, {text('session', empty)}, "
@@ -689,10 +695,73 @@ class CallVisitor:
689
695
 
690
696
  def _array_method_arg_nodes(self, method: str, node: FuncCall) -> list:
691
697
  """Merge checked-array method kwargs into Pine signature order."""
692
- param_names = CHECKED_ARRAY_METHOD_KWARGS.get(method)
693
- 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:
694
713
  return list(node.args)
695
- 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
696
765
 
697
766
  def _matrix_bool_value_args(
698
767
  self, method: str, args: list[str], arg_nodes: list,
@@ -739,12 +808,7 @@ class CallVisitor:
739
808
  "array.copy: expected exactly one receiver 'id'",
740
809
  hint="Use array.copy(source) or array.copy(id=source).",
741
810
  )
742
- param_names = CHECKED_ARRAY_METHOD_KWARGS.get(method)
743
- if param_names is None:
744
- return list(node.args)
745
- return _merge_kwargs(
746
- node.args, node.kwargs, ["id", *param_names], lambda arg: arg
747
- )
811
+ return self._collection_arg_nodes("array", method, node, True)
748
812
 
749
813
  def _map_identifier_is_visible_binding(self, node: FuncCall) -> bool:
750
814
  """Whether ``map`` is a lexical value at this exact source position.
@@ -1662,6 +1726,18 @@ class CallVisitor:
1662
1726
  return self._matrix_history_method_expr(
1663
1727
  callee.object, callee.member, recv_spec, node)
1664
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
+
1665
1741
  # chart.point.now/new/from_index/from_time/copy — REAL data (a ChartPoint
1666
1742
  # aggregate). Routed here BEFORE the obj.field.method receiver logic,
1667
1743
  # which would otherwise mis-treat ``chart.point`` as a receiver object
@@ -1820,20 +1896,16 @@ class CallVisitor:
1820
1896
  ):
1821
1897
  arr = self._collection_receiver_expr(oname)
1822
1898
  self._check_matrix_method_allowed(meth_raw, recv_spec, node)
1823
- param_names = MATRIX_METHOD_KWARGS.get(meth_raw)
1824
- raw_args = (
1825
- _merge_kwargs(node.args, node.kwargs, param_names, lambda a: a)
1826
- if param_names and node.kwargs else list(node.args)
1827
- )
1899
+ raw_args = self._matrix_method_arg_nodes(meth_raw, node)
1828
1900
  margs = self._matrix_bool_value_args(
1829
1901
  meth_raw,
1830
- [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],
1831
1903
  raw_args,
1832
1904
  recv_spec,
1833
1905
  )
1834
1906
  fn = MATRIX_METHODS[meth_raw]
1835
1907
  try:
1836
- return fn(arr, margs)
1908
+ return self._matrix_method_expr(arr, meth_raw, margs, raw_args, node)
1837
1909
  except IndexError:
1838
1910
  self._codegen_error(
1839
1911
  node,
@@ -2053,20 +2125,16 @@ class CallVisitor:
2053
2125
  ):
2054
2126
  arr = self._collection_receiver_expr(namespace)
2055
2127
  self._check_matrix_method_allowed(func_name, namespace_spec, node)
2056
- param_names = MATRIX_METHOD_KWARGS.get(func_name)
2057
- raw_args = (
2058
- _merge_kwargs(node.args, node.kwargs, param_names, lambda a: a)
2059
- if param_names and node.kwargs else list(node.args)
2060
- )
2128
+ raw_args = self._matrix_method_arg_nodes(func_name, node)
2061
2129
  args = self._matrix_bool_value_args(
2062
2130
  func_name,
2063
- [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],
2064
2132
  raw_args,
2065
2133
  namespace_spec,
2066
2134
  )
2067
2135
  fn = MATRIX_METHODS[func_name]
2068
2136
  try:
2069
- return fn(arr, args)
2137
+ return self._matrix_method_expr(arr, func_name, args, raw_args, node)
2070
2138
  except IndexError:
2071
2139
  self._codegen_error(
2072
2140
  node,
@@ -2107,7 +2175,22 @@ class CallVisitor:
2107
2175
  init_val = self._array_init_value_expr(elem_spec, node.args[1])
2108
2176
  else:
2109
2177
  init_val = init_default
2110
- 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
+ )
2111
2194
  return f"{cpp_type}()"
2112
2195
  if func_name == "from":
2113
2196
  spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float"))
@@ -2281,8 +2364,8 @@ class CallVisitor:
2281
2364
  # runtime.error() and other runtime.* calls
2282
2365
  if namespace == "runtime":
2283
2366
  if func_name == "error":
2284
- rt_args = [self._visit_expr(a) for a in node.args]
2285
- 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 '""'
2286
2369
  return f'pine_runtime_error({msg_arg})'
2287
2370
  return '"" /* unsupported runtime */'
2288
2371
 
@@ -2614,10 +2697,31 @@ class CallVisitor:
2614
2697
  return f"na<double>() /* unsupported: ta.{func_name} */"
2615
2698
 
2616
2699
  if namespace == "syminfo":
2617
- if func_name == "prefix":
2618
- return "_pf_derive_prefix(syminfo_.tickerid)"
2619
- if func_name == "ticker":
2620
- 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
+ )
2621
2725
  return f"na<double>() /* unsupported: syminfo.{func_name} */"
2622
2726
 
2623
2727
  # str.* fallback now handled by _visit_str_call above
@@ -2642,41 +2746,48 @@ class CallVisitor:
2642
2746
  if len(args_e) > 2 else self._default_for_spec(elem_spec)
2643
2747
  )
2644
2748
  return f"PineGenericMatrix<{cpp_t}>::new_({rows}, {cols}, {init})"
2645
- 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:
2646
2755
  from ..ast_nodes import Identifier as _Ident
2647
2756
  if func_name in MATRIX_NUMERIC_ONLY:
2648
- recv_node = node.args[0]
2757
+ recv_node = matrix_receiver
2649
2758
  if history_annotation(recv_node) is not None:
2650
2759
  # A matrix's history is a matrix of its type.
2651
2760
  recv_node = recv_node.object
2652
- if not isinstance(recv_node, _Ident):
2653
- self._codegen_error(node, f"matrix.{func_name} receiver must be a variable reference")
2654
- recv_name = recv_node.name
2655
- recv_spec = self._collection_spec_for_name(recv_name)
2656
- if recv_spec is None or recv_spec.kind != "matrix":
2657
- 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")
2658
2773
  self._check_matrix_method_allowed(func_name, recv_spec, node)
2659
2774
  if func_name == "sort":
2660
- if isinstance(node.args[0], _Ident):
2661
- recv_name = node.args[0].name
2775
+ if isinstance(matrix_receiver, _Ident):
2776
+ recv_name = matrix_receiver.name
2662
2777
  recv_spec = self._collection_spec_for_name(recv_name)
2663
2778
  if recv_spec is not None and recv_spec.kind == "matrix":
2664
2779
  self._check_matrix_method_allowed(func_name, recv_spec, node)
2665
- obj = self._visit_expr(node.args[0])
2666
- param_names = MATRIX_METHOD_KWARGS.get(func_name)
2667
- raw_rest = (
2668
- _merge_kwargs(node.args[1:], node.kwargs, param_names, lambda a: a)
2669
- if param_names else list(node.args[1:])
2670
- )
2780
+ obj = self._visit_expr(matrix_receiver)
2781
+ raw_rest = self._matrix_method_arg_nodes(func_name, node, True)
2671
2782
  rest = self._matrix_bool_value_args(
2672
2783
  func_name,
2673
- [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],
2674
2785
  raw_rest,
2675
- self._type_spec_from_expr(node.args[0]),
2786
+ self._type_spec_from_expr(matrix_receiver),
2676
2787
  )
2677
2788
  fn = MATRIX_METHODS[func_name]
2678
2789
  try:
2679
- return fn(obj, rest)
2790
+ return self._matrix_method_expr(obj, func_name, rest, raw_rest, node)
2680
2791
  except IndexError:
2681
2792
  self._codegen_error(
2682
2793
  node,
@@ -3166,6 +3277,7 @@ class CallVisitor:
3166
3277
  return f"{token}ULL"
3167
3278
 
3168
3279
  def _visit_strategy_call(self, func_name: str, node: FuncCall) -> str:
3280
+ from .order_shapes import record_order_call
3169
3281
  if func_name in ("convert_to_account", "convert_to_symbol"):
3170
3282
  p = self._resolve_func_args(node, f"strategy.{func_name}")
3171
3283
  v = self._visit_expr(p.get("value")) if p.get("value") is not None else "0.0"
@@ -3200,6 +3312,16 @@ class CallVisitor:
3200
3312
  if stop is not None or limit is not None or qty is not None or oca_name is not None or oca_type is not None or qty_type is not None:
3201
3313
  limit_val = self._visit_expr(limit) if limit else "na<double>()"
3202
3314
  stop_val = self._visit_expr(stop) if stop else "na<double>()"
3315
+ record_order_call(self, node, "entry", {
3316
+ "id": (entry_id, p.get("id")), "direction": (direction, direction_node),
3317
+ "limit": (limit_val, limit, "na<double>()"),
3318
+ "stop": (stop_val, stop, "na<double>()"),
3319
+ "qty": (qty_val, qty, "na<double>()"),
3320
+ "comment": (comment_val, comment, '""'),
3321
+ "oca_name": (oca_name_val, oca_name, '""'),
3322
+ "oca_type": (oca_type_val, oca_type, "0"),
3323
+ "qty_type": (qty_type_val, qty_type, "-1"),
3324
+ })
3203
3325
  # pineforge-engine v0.2 dropped the vestigial `market_price`
3204
3326
  # third positional from `BacktestEngine::strategy_entry`
3205
3327
  # (the runtime never read it; fill price always came from
@@ -3207,6 +3329,15 @@ class CallVisitor:
3207
3329
  # matches the new signature: (id, direction, limit, stop,
3208
3330
  # qty, comment, oca_name, oca_type, qty_type).
3209
3331
  return f"strategy_entry({entry_id}, {direction}, {limit_val}, {stop_val}, {qty_val}, {comment_val}, {oca_name_val}, {oca_type_val}, {qty_type_val})"
3332
+ record_order_call(self, node, "entry", {
3333
+ "id": (entry_id, p.get("id")), "direction": (direction, direction_node),
3334
+ "limit": ("na<double>()", None, "na<double>()"),
3335
+ "stop": ("na<double>()", None, "na<double>()"),
3336
+ "qty": ("na<double>()", None, "na<double>()"),
3337
+ "comment": (comment_val, comment, '""'),
3338
+ "oca_name": ('""', None, '""'), "oca_type": ("0", None, "0"),
3339
+ "qty_type": ("-1", None, "-1"),
3340
+ })
3210
3341
  return f"strategy_entry({entry_id}, {direction}, na<double>(), na<double>(), na<double>(), {comment_val})"
3211
3342
 
3212
3343
  if func_name == "close":
@@ -3223,6 +3354,12 @@ class CallVisitor:
3223
3354
  if immediately_node is not None else "false"
3224
3355
  )
3225
3356
  callsite = self._strategy_close_callsite_token(node)
3357
+ record_order_call(self, node, "close", {
3358
+ "id": (close_id, p.get("id")), "comment": (comment, p.get("comment"), '""'),
3359
+ "qty": (qty, p.get("qty"), "na<double>()"),
3360
+ "qty_percent": (qty_pct, p.get("qty_percent"), "na<double>()"),
3361
+ "immediately": (immediately, immediately_node, "false"),
3362
+ })
3226
3363
  return f"strategy_close({close_id}, {comment}, {qty}, {qty_pct}, {immediately}, {callsite})"
3227
3364
 
3228
3365
  if func_name == "close_all":
@@ -3235,6 +3372,12 @@ class CallVisitor:
3235
3372
  )
3236
3373
  if immediately_node is not None else "false"
3237
3374
  )
3375
+ record_order_call(self, node, "close_all", {
3376
+ "comment": (comment, p.get("comment"), '""'),
3377
+ "qty": ("na<double>()", None, "na<double>()"),
3378
+ "qty_percent": ("na<double>()", None, "na<double>()"),
3379
+ "immediately": (immediately, immediately_node, "false"),
3380
+ })
3238
3381
  # The engine's ID-less strategy_close path closes the complete
3239
3382
  # position. Reuse it so close_all preserves the Pine order comment
3240
3383
  # and same-tick fill flag instead of silently discarding both.
@@ -3278,18 +3421,38 @@ class CallVisitor:
3278
3421
  if loss_n and not stop_n:
3279
3422
  loss_ticks = self._visit_expr(loss_n)
3280
3423
 
3424
+ record_order_call(self, node, "exit", {
3425
+ "id": (exit_id, p.get("id")), "from_entry": (from_id, p.get("from_entry")),
3426
+ "limit": (limit_val, limit_n, "na<double>()"),
3427
+ "stop": (stop_val, stop_n, "na<double>()"),
3428
+ "trail_points": (trail_pts, trail_pts_n, "na<double>()"),
3429
+ "trail_offset": (trail_off, trail_off_n, "na<double>()"),
3430
+ "trail_price": (trail_pr, trail_pr_n, "na<double>()"),
3431
+ "qty_percent": (qty_pct, qty_pct_n, "100.0"),
3432
+ "comment": (comment, comment_n, '""'),
3433
+ "qty": (qty_val, qty_n, "na<double>()"),
3434
+ "oca_name": (oca_val, oca_name_n, '""'),
3435
+ "profit_ticks": (profit_ticks, profit_n if not limit_n else None, "na<double>()"),
3436
+ "loss_ticks": (loss_ticks, loss_n if not stop_n else None, "na<double>()"),
3437
+ }, form="levels")
3281
3438
  return (f"strategy_exit({exit_id}, {from_id}, {limit_val}, {stop_val}, "
3282
3439
  f"{trail_pts}, {trail_off}, {trail_pr}, {qty_pct}, {comment}, "
3283
3440
  f"{qty_val}, {oca_val}, {profit_ticks}, {loss_ticks})")
3284
3441
  comment = self._visit_expr(comment_n) if comment_n is not None else '""'
3442
+ record_order_call(self, node, "exit", {
3443
+ "id": (exit_id, p.get("id")), "from_entry": (from_id, p.get("from_entry")),
3444
+ "comment": (comment, comment_n, '""'),
3445
+ }, form="cancel_bracket")
3285
3446
  return f"strategy_exit_cancel_bracket({exit_id}, {from_id}, {comment})"
3286
3447
 
3287
3448
  if func_name == "cancel":
3288
3449
  p = self._resolve_func_args(node, "strategy.close") # same shape: id first
3289
3450
  cancel_id = self._visit_expr(p.get("id")) if "id" in p else '""'
3451
+ record_order_call(self, node, "cancel", {"id": (cancel_id, p.get("id"))})
3290
3452
  return f"strategy_cancel({cancel_id})"
3291
3453
 
3292
3454
  if func_name == "cancel_all":
3455
+ record_order_call(self, node, "cancel_all", {})
3293
3456
  return "strategy_cancel_all()"
3294
3457
 
3295
3458
  if func_name == "order":
@@ -3307,6 +3470,14 @@ class CallVisitor:
3307
3470
  stop_arg = self._visit_expr(p.get("stop")) if "stop" in p else "na<double>()"
3308
3471
  oca_name = self._visit_expr(p.get("oca_name")) if "oca_name" in p else '""'
3309
3472
  oca_type = self._visit_expr(p.get("oca_type")) if "oca_type" in p else "0"
3473
+ record_order_call(self, node, "order", {
3474
+ "id": (order_id, p.get("id")), "direction": (direction, direction_node),
3475
+ "qty": (qty, p.get("qty"), "0"),
3476
+ "limit": (limit_arg, p.get("limit"), "na<double>()"),
3477
+ "stop": (stop_arg, p.get("stop"), "na<double>()"),
3478
+ "oca_name": (oca_name, p.get("oca_name"), '""'),
3479
+ "oca_type": (oca_type, p.get("oca_type"), "0"),
3480
+ })
3310
3481
  return f"strategy_order({order_id}, {direction}, {qty}, {limit_arg}, {stop_arg}, {oca_name}, {oca_type})"
3311
3482
 
3312
3483
  if func_name == "risk":
@@ -3570,13 +3741,22 @@ class CallVisitor:
3570
3741
  return 'std::string("")'
3571
3742
 
3572
3743
  if func_name == "substring":
3573
- if len(args) == 3:
3574
- # 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
+
3575
3758
  return evaluate_args_once(
3576
- args, (1,), lambda a: f"{a[0]}.substr({a[1]}, {a[2]} - {a[1]})",
3577
- "_pf_str_a")
3578
- elif len(args) == 2:
3579
- return f"{args[0]}.substr({args[1]})"
3759
+ args, range(len(args)), checked_substring, "_pf_substring_arg")
3580
3760
  return 'std::string("")'
3581
3761
 
3582
3762
  if func_name == "format":