@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
@@ -565,6 +565,7 @@ NAME_ECHO_STRING_MEMBERS = {
565
565
 
566
566
  def _checked_array_index_prelude(
567
567
  *,
568
+ method: str,
568
569
  normalize_negative: bool = True,
569
570
  allow_size: bool = False,
570
571
  name: str = "index",
@@ -602,18 +603,18 @@ def _checked_array_index_prelude(
602
603
  f"using {raw_type}=std::decay_t<decltype({raw_value})>; "
603
604
  f"if constexpr(!std::is_same_v<{raw_type},bool>) {{ "
604
605
  f"if(is_na({raw_value})) "
605
- "pine_runtime_error(std::string(\"Index na is out of bounds. Array size is \")+"
606
+ f'_PF_ARRAY_STOP("index_out_of_bounds", "{method}", std::string("Index na is out of bounds. Array size is ")+ '
606
607
  "std::to_string((int64_t)__pf_array.size())); } "
607
608
  f"if constexpr(std::is_floating_point_v<{raw_type}>) {{ "
608
609
  f"if(!std::isfinite({raw_value})) {{ "
609
610
  f"std::string {raw_text}={raw_value}>0?\"inf\":\"-inf\"; "
610
- f"pine_runtime_error(std::string(\"Index \")+{raw_text}+"
611
+ f'_PF_ARRAY_STOP("index_out_of_bounds", "{method}", std::string("Index ")+{raw_text}+'
611
612
  "\" is out of bounds. Array size is \"+"
612
613
  "std::to_string((int64_t)__pf_array.size())); } "
613
614
  f"long double {raw_wide}=(long double){raw_value}; "
614
615
  f"if({raw_wide}<(long double)std::numeric_limits<int64_t>::min()||"
615
616
  f"{raw_wide}>(long double)std::numeric_limits<int64_t>::max()) "
616
- "pine_runtime_error(std::string(\"Index \")+"
617
+ f'_PF_ARRAY_STOP("index_out_of_bounds", "{method}", std::string("Index ")+ '
617
618
  f"std::to_string((double){raw_value})+"
618
619
  "\" is out of bounds. Array size is \"+"
619
620
  "std::to_string((int64_t)__pf_array.size())); } "
@@ -621,12 +622,12 @@ def _checked_array_index_prelude(
621
622
  f"int64_t {size}=(int64_t)__pf_array.size(); "
622
623
  f"int64_t {checked}={checked_index}; "
623
624
  f"if({checked}<0||{checked}{upper}{size}) "
624
- f"pine_runtime_error(std::string(\"Index \")+std::to_string({raw})+"
625
+ f'_PF_ARRAY_STOP("index_out_of_bounds", "{method}", std::string("Index ")+std::to_string({raw})+'
625
626
  f"\" is out of bounds. Array size is \"+std::to_string({size})); "
626
627
  )
627
628
 
628
629
 
629
- def _checked_array_range_prelude(*, reject_inverted: bool = True) -> str:
630
+ def _checked_array_range_prelude(*, method: str, reject_inverted: bool = True) -> str:
630
631
  """Validate the half-open ``[index_from, index_to)`` range of fill/slice.
631
632
 
632
633
  Both endpoints are checked with ``allow_size`` (``index_to`` is exclusive,
@@ -637,14 +638,14 @@ def _checked_array_range_prelude(*, reject_inverted: bool = True) -> str:
637
638
  """
638
639
  return (
639
640
  _checked_array_index_prelude(
640
- normalize_negative=False, allow_size=True, name="index_from"
641
+ method=method, normalize_negative=False, allow_size=True, name="index_from"
641
642
  )
642
643
  + _checked_array_index_prelude(
643
- normalize_negative=False, allow_size=True, name="index_to"
644
+ method=method, normalize_negative=False, allow_size=True, name="index_to"
644
645
  )
645
646
  + (
646
647
  "if(__pf_array_index_from>__pf_array_index_to) "
647
- "pine_runtime_error(\"Index 'from' should be less than index 'to'.\"); "
648
+ '_PF_ARRAY_STOP("slice_range_inverted", "slice", "Index \'from\' should be less than index \'to\'."); '
648
649
  if reject_inverted else ""
649
650
  )
650
651
  )
@@ -656,7 +657,7 @@ def _checked_array_insert(a: str, args: list[str], normalize_negative: bool = Tr
656
657
  ``index == size`` appends, so the bound is ``index <= size``; a negative
657
658
  index is end-relative, exactly as for ``get``/``set``/``remove``.
658
659
  """
659
- check = _checked_array_index_prelude(allow_size=True,
660
+ check = _checked_array_index_prelude(method="insert", allow_size=True,
660
661
  normalize_negative=normalize_negative)
661
662
  return (
662
663
  "[&](auto&& __pf_array){ "
@@ -671,7 +672,7 @@ def _checked_array_insert(a: str, args: list[str], normalize_negative: bool = Tr
671
672
 
672
673
  def _checked_array_fill_range(a: str, args: list[str]) -> str:
673
674
  """``array.fill(id, value, index_from, index_to)`` — bounded range fill."""
674
- check = _checked_array_range_prelude(reject_inverted=False)
675
+ check = _checked_array_range_prelude(method="fill", reject_inverted=False)
675
676
  return (
676
677
  "[&](auto&& __pf_array){ "
677
678
  "return [&](auto&& __pf_array_value){ "
@@ -691,7 +692,7 @@ def checked_array_slice(a: str, args: list[str], *, result_type: str) -> str:
691
692
  ``result_type`` stays caller-supplied so the typed method lane keeps
692
693
  emitting the receiver's own element type; only the bounds checks are new.
693
694
  """
694
- check = _checked_array_range_prelude()
695
+ check = _checked_array_range_prelude(method="slice")
695
696
  return (
696
697
  "[&](auto&& __pf_array){ "
697
698
  "return [&](auto&& __pf_raw_index_from_value){ "
@@ -704,7 +705,7 @@ def checked_array_slice(a: str, args: list[str], *, result_type: str) -> str:
704
705
 
705
706
 
706
707
  def _checked_array_get(a: str, args: list[str], normalize_negative: bool = True) -> str:
707
- check = _checked_array_index_prelude(normalize_negative=normalize_negative)
708
+ check = _checked_array_index_prelude(method="get", normalize_negative=normalize_negative)
708
709
  return (
709
710
  "[&](auto&& __pf_array)->decltype(auto){ "
710
711
  "return [&](auto&& __pf_raw_index_value)->decltype(auto){ "
@@ -719,7 +720,7 @@ def _checked_array_get(a: str, args: list[str], normalize_negative: bool = True)
719
720
 
720
721
 
721
722
  def _checked_array_set(a: str, args: list[str], normalize_negative: bool = True) -> str:
722
- check = _checked_array_index_prelude(normalize_negative=normalize_negative)
723
+ check = _checked_array_index_prelude(method="set", normalize_negative=normalize_negative)
723
724
  return (
724
725
  "[&](auto&& __pf_array){ "
725
726
  "return [&](auto&& __pf_raw_index_value){ "
@@ -731,7 +732,7 @@ def _checked_array_set(a: str, args: list[str], normalize_negative: bool = True)
731
732
 
732
733
 
733
734
  def _checked_array_remove(a: str, args: list[str], normalize_negative: bool = True) -> str:
734
- check = _checked_array_index_prelude(normalize_negative=normalize_negative)
735
+ check = _checked_array_index_prelude(method="remove", normalize_negative=normalize_negative)
735
736
  return (
736
737
  "[&](auto&& __pf_array){ "
737
738
  "return [&](auto&& __pf_raw_index_value){ "
@@ -750,7 +751,7 @@ def _checked_array_end_get(a: str, method: str) -> str:
750
751
  access = "front" if method == "first" else "back"
751
752
  return (
752
753
  "[&](auto&& __pf_array)->decltype(auto){ "
753
- f"if(__pf_array.empty()) pine_runtime_error(\"Cannot use {method}() "
754
+ f'if(__pf_array.empty()) _PF_ARRAY_STOP("empty_array_access", "{method}", "Cannot use {method}() '
754
755
  "if array is empty.\"); "
755
756
  "if constexpr(std::is_lvalue_reference_v<decltype(__pf_array)>) "
756
757
  f"return (__pf_array.{access}()); "
@@ -770,7 +771,7 @@ def _checked_array_end_remove(a: str, method: str) -> str:
770
771
  )
771
772
  return (
772
773
  "[&](auto&& __pf_array){ "
773
- f"if(__pf_array.empty()) pine_runtime_error(\"Cannot use {method}() "
774
+ f'if(__pf_array.empty()) _PF_ARRAY_STOP("empty_array_access", "{method}", "Cannot use {method}() '
774
775
  "if array is empty.\"); "
775
776
  "using __pf_array_value_type=typename "
776
777
  "std::decay_t<decltype(__pf_array)>::value_type; "
@@ -781,7 +782,7 @@ def _checked_array_end_remove(a: str, method: str) -> str:
781
782
 
782
783
  def _checked_array_percentrank(a: str, args: list[str]) -> str:
783
784
  """Preserve degenerate results, then reject invalid PercentRank indices."""
784
- check = _checked_array_index_prelude(normalize_negative=False)
785
+ check = _checked_array_index_prelude(method="percentrank", normalize_negative=False)
785
786
  return (
786
787
  "[&](auto&& __pf_array){ "
787
788
  "return [&](auto&& __pf_raw_index_value){ "
@@ -822,7 +823,7 @@ ARRAY_METHODS = {
822
823
  "size": lambda a, args: f"(double){a}.size()",
823
824
  "clear": lambda a, args: f"{a}.clear()",
824
825
  "fill": lambda a, args: f"std::fill({a}.begin(), {a}.end(), {args[0]})" if len(args) == 1
825
- else _checked_array_fill_range(a, args),
826
+ else _checked_array_fill_range(a, args + [f"(int){a}.size()"] if len(args) == 2 else args),
826
827
  "includes": lambda a, args: f"(std::find({a}.begin(), {a}.end(), {args[0]}) != {a}.end())",
827
828
  "indexof": lambda a, args: f"[&](){{ auto __pf_it=std::find({a}.begin(),{a}.end(),{args[0]}); return __pf_it!={a}.end()?(double)(__pf_it-{a}.begin()):-1.0; }}()",
828
829
  "lastindexof": lambda a, args: f"[&](){{ for(int __pf_i=(int){a}.size()-1;__pf_i>=0;__pf_i--)if({a}[__pf_i]=={args[0]})return(double)__pf_i; return -1.0; }}()",
@@ -900,6 +901,9 @@ CHECKED_ARRAY_METHOD_KWARGS: dict[str, list[str]] = {
900
901
  "last": [],
901
902
  "pop": [],
902
903
  "shift": [],
904
+ "sort": ["order"],
905
+ "indexof": ["value"],
906
+ "unshift": ["value"],
903
907
  }
904
908
 
905
909
  MAP_METHODS = {
@@ -954,10 +958,109 @@ def _matrix_add_col(m: str, args: list) -> str:
954
958
  raise IndexError("matrix.add_col")
955
959
 
956
960
 
957
- # Keyword parameter order for matrix methods (Pine v6); used by ``_merge_kwargs``.
961
+ def _matrix_sum(receiver: str, arguments: list[str]) -> str:
962
+ """Use the runtime's exposed Eigen arithmetic for matrix/scalar sums."""
963
+ if not arguments:
964
+ return f"{receiver}.sum()"
965
+ if len(arguments) != 1:
966
+ raise IndexError("matrix.sum")
967
+ return (
968
+ "([](const auto& _pf_sum_left, const auto& _pf_sum_right) { "
969
+ "auto _pf_sum_result = _pf_sum_left.copy(); "
970
+ "if constexpr (std::is_arithmetic_v<std::decay_t<decltype(_pf_sum_right)>>) { "
971
+ "_pf_sum_result.data().array() += _pf_sum_right; "
972
+ "} else { "
973
+ "if (_pf_sum_left.rows() != _pf_sum_right.rows() || "
974
+ "_pf_sum_left.columns() != _pf_sum_right.columns()) "
975
+ 'throw std::runtime_error("Cannot sum matrices with different dimensions."); '
976
+ "_pf_sum_result.data() += _pf_sum_right.data(); "
977
+ "} return _pf_sum_result; "
978
+ f"}}({receiver}, {arguments[0]}))"
979
+ )
980
+
981
+
982
+ def _matrix_submatrix(receiver: str, arguments: list[str]) -> str:
983
+ if len(arguments) > 4:
984
+ raise IndexError("matrix.submatrix")
985
+ if len(arguments) == 4 and all(argument is not None for argument in arguments):
986
+ return f"{receiver}.submatrix({', '.join(_matrix_int_arg(argument) for argument in arguments)})"
987
+ defaults = ["0", "_pf_matrix_target.rows()", "0", "_pf_matrix_target.columns()"]
988
+ arguments = arguments + [None] * (4 - len(arguments))
989
+ bounds = [argument if argument is not None else defaults[index]
990
+ for index, argument in enumerate(arguments)]
991
+ return ("[&](auto&& _pf_matrix_target) { return _pf_matrix_target.submatrix("
992
+ f"{', '.join(_matrix_int_arg(argument) for argument in bounds)}); }}({receiver})")
993
+
994
+
995
+ # Parameter order of the matrix methods (Pine v6), receiver excluded: the
996
+ # slots ``CallVisitor._collection_arg_nodes`` binds keyword arguments to. A
997
+ # method absent here takes only its receiver. ``fill`` names its range
998
+ # parameters so that a keyword one binds as a positional one does: the
999
+ # lowering reads only ``value`` (the engine fills the whole matrix), a
1000
+ # pre-existing approximation of both spellings that the call does not refuse.
958
1001
  MATRIX_METHOD_KWARGS: dict[str, list[str]] = {
959
- "add_row": ["row_index", "array_id"],
960
- "add_col": ["col_index", "array_id"],
1002
+ "add_row": ["row", "array_id"],
1003
+ "add_col": ["column", "array_id"],
1004
+ "sort": ["column", "order"],
1005
+ "submatrix": ["from_row", "to_row", "from_column", "to_column"],
1006
+ "sum": ["id2"],
1007
+ "diff": ["id2"],
1008
+ "mult": ["id2"],
1009
+ "get": ["row", "column"],
1010
+ "set": ["row", "column", "value"],
1011
+ "fill": ["value", "from_row", "to_row", "from_column", "to_column"],
1012
+ "row": ["row"],
1013
+ "col": ["column"],
1014
+ "remove_row": ["row"],
1015
+ "remove_col": ["column"],
1016
+ "swap_rows": ["row1", "row2"],
1017
+ "swap_columns": ["column1", "column2"],
1018
+ "reshape": ["rows", "columns"],
1019
+ "concat": ["id2", "vertical"],
1020
+ "pow": ["power"],
1021
+ "kron": ["id2"],
1022
+ }
1023
+
1024
+ # Keyword spellings an earlier build bound that Pine's reference does not
1025
+ # name; they keep binding to the same slot.
1026
+ MATRIX_METHOD_KWARG_ALIASES: dict[str, dict[str, str]] = {
1027
+ "add_row": {"row_index": "row"},
1028
+ "add_col": {"col_index": "column"},
1029
+ }
1030
+
1031
+ # The receiver's own keyword in the namespace form (``matrix.diff(id1=m, ...)``).
1032
+ MATRIX_RECEIVER_KEYWORD: dict[str, str] = {
1033
+ "sum": "id1", "diff": "id1", "mult": "id1", "concat": "id1", "kron": "id1",
1034
+ }
1035
+
1036
+
1037
+ def _matrix_functional_receiver(method: str, node):
1038
+ """The receiver node of ``matrix.<method>(...)``: its first positional
1039
+ argument, else its keyword."""
1040
+ return node.args[0] if node.args else node.kwargs.get(
1041
+ MATRIX_RECEIVER_KEYWORD.get(method, "id"))
1042
+
1043
+
1044
+ # The optional parameters of the collection methods whose keywords bind by
1045
+ # position, and what a gap left before one stands for when only a later
1046
+ # keyword is written (``m.sort(order = order.descending)`` leaves ``column``
1047
+ # open): a number is Pine's default, ``None`` an omitted argument the method's
1048
+ # template defaults from the receiver's own extent (``_matrix_submatrix``'s
1049
+ # ``to_row`` / ``to_column``, the defaulted ``add_row`` / ``add_col`` of
1050
+ # ``TypeInferer._matrix_method_expr``). A parameter that is not listed is
1051
+ # required: a gap before it is refused.
1052
+ MATRIX_OPTIONAL_PARAMS: dict[str, dict[str, int | None]] = {
1053
+ "sort": {"column": 0, "order": None},
1054
+ "submatrix": {"from_row": 0, "to_row": None, "from_column": 0, "to_column": None},
1055
+ "add_row": {"row": None, "array_id": None},
1056
+ "add_col": {"column": None, "array_id": None},
1057
+ "fill": {"from_row": None, "to_row": None, "from_column": None, "to_column": None},
1058
+ "sum": {"id2": None},
1059
+ "concat": {"vertical": None},
1060
+ }
1061
+ ARRAY_OPTIONAL_PARAMS: dict[str, dict[str, int | None]] = {
1062
+ "fill": {"index_from": 0, "index_to": None},
1063
+ "sort": {"order": None},
961
1064
  }
962
1065
 
963
1066
  # Matrix mutators whose established C++ lowering returns ``void``. A Pine
@@ -1024,17 +1127,17 @@ MATRIX_METHODS = {
1024
1127
  "swap_rows": lambda m, args: f"{m}.swap_rows({_matrix_int_arg(args[0])}, {_matrix_int_arg(args[1])})",
1025
1128
  "swap_columns": lambda m, args: f"{m}.swap_columns({_matrix_int_arg(args[0])}, {_matrix_int_arg(args[1])})",
1026
1129
  "copy": lambda m, args: f"{m}.copy()",
1027
- "submatrix": lambda m, args: f"{m}.submatrix({_matrix_int_arg(args[0])}, {_matrix_int_arg(args[1])}, {_matrix_int_arg(args[2])}, {_matrix_int_arg(args[3])})",
1130
+ "submatrix": _matrix_submatrix,
1028
1131
  "reshape": lambda m, args: f"{m}.reshape({_matrix_int_arg(args[0])}, {_matrix_int_arg(args[1])})",
1029
1132
  "reverse": lambda m, args: f"{m}.reverse()",
1030
1133
  "transpose": lambda m, args: f"{m}.transpose()",
1031
- "sort": lambda m, args: f"{m}.sort({_matrix_int_arg(args[0])}, {args[1]} != \"descending\")" if len(args)>1 else f"{m}.sort({_matrix_int_arg(args[0])})",
1134
+ "sort": lambda m, args: f"{m}.sort({_matrix_int_arg(args[0])}, {args[1]} != \"descending\")" if len(args)>1 else f"{m}.sort({_matrix_int_arg(args[0])})" if args else f"{m}.sort(0)",
1032
1135
  "concat": lambda m, args: f"{m}.concat({args[0]}, {pine_truth_cast(args[1])})" if len(args)>1 else f"{m}.concat({args[0]}, true)",
1033
1136
  "avg": lambda m, args: f"{m}.avg()",
1034
1137
  "min": lambda m, args: f"{m}.min()",
1035
1138
  "max": lambda m, args: f"{m}.max()",
1036
1139
  "mode": lambda m, args: f"{m}.mode()",
1037
- "sum": lambda m, args: f"{m}.sum()",
1140
+ "sum": _matrix_sum,
1038
1141
  "diff": lambda m, args: f"{m}.diff({args[0]})",
1039
1142
  "mult": lambda m, args: f"{m}.mult({args[0]})",
1040
1143
  "pow": lambda m, args: f"{m}.pow({_matrix_int_arg(args[0])})",
@@ -1128,10 +1231,11 @@ MATH_FUNC_MAP = {
1128
1231
  STR_FUNC_MAP = {
1129
1232
  "tostring": None, # handled separately (already works)
1130
1233
  "tonumber": lambda args: (
1131
- f"[&](){{ "
1132
- f"try {{ return std::stod({args[0]}); }} "
1133
- f"catch (...) {{ return na<double>(); }} "
1134
- f"}}()"
1234
+ f"[&](std::string _pf_number_text){{ "
1235
+ f"try {{ return std::stod(_pf_number_text); }} "
1236
+ f"catch (const std::invalid_argument&) {{ return na<double>(); }} "
1237
+ f"catch (const std::out_of_range&) {{ return na<double>(); }} "
1238
+ f"}}(({args[0]}))"
1135
1239
  ),
1136
1240
  "length": lambda args: f"(int){args[0]}.length()",
1137
1241
  "contains": lambda args: f"({args[0]}.find({args[1]}) != std::string::npos)",
@@ -60,7 +60,7 @@ static std::string _pf_tv_decimal(double value, int min_fraction,
60
60
  char buffer[128];
61
61
  const auto converted = std::to_chars(buffer, buffer + sizeof buffer, value);
62
62
  if (converted.ec != std::errc{})
63
- throw std::runtime_error("shortest-decimal conversion failed");
63
+ _PF_ENGINE_INVARIANT("shortest-decimal conversion failed", std::runtime_error);
64
64
  std::string spelling(buffer, converted.ptr);
65
65
  const bool negative = !spelling.empty() && spelling[0] == '-';
66
66
  if (negative) spelling.erase(0, 1);
@@ -240,7 +240,14 @@ static std::string pine_str_format_tv(
240
240
  i = end + 1;
241
241
  continue;
242
242
  }
243
- const size_t index = static_cast<size_t>(std::stoul(index_text));
243
+ size_t index = 0;
244
+ for (char _pf_digit : index_text) {
245
+ const size_t _pf_value = static_cast<size_t>(_pf_digit - '0');
246
+ if (index > (std::numeric_limits<size_t>::max() - _pf_value) / 10) {
247
+ _PF_STRING_STOP("format_index_overflow", "stoul");
248
+ }
249
+ index = index * 10 + _pf_value;
250
+ }
244
251
  if (index >= args.size()) {
245
252
  result += format_string.substr(i, end - i + 1);
246
253
  i = end + 1;
@@ -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