@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.
- package/LICENSE +19 -3
- package/README.md +2 -1
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +6 -5
- package/pineforge_codegen/analyzer/base.py +3 -3
- package/pineforge_codegen/analyzer/types.py +7 -3
- package/pineforge_codegen/codegen/base.py +36 -17
- package/pineforge_codegen/codegen/capabilities.py +143 -10
- package/pineforge_codegen/codegen/checked_settings.py +161 -4
- package/pineforge_codegen/codegen/collection_history.py +163 -12
- package/pineforge_codegen/codegen/emit_top.py +29 -13
- package/pineforge_codegen/codegen/helpers.py +25 -1
- package/pineforge_codegen/codegen/host_members.py +1 -0
- package/pineforge_codegen/codegen/input.py +23 -8
- package/pineforge_codegen/codegen/order_shapes.py +356 -0
- package/pineforge_codegen/codegen/run_stops.py +52 -0
- package/pineforge_codegen/codegen/security.py +66 -10
- package/pineforge_codegen/codegen/tables.py +144 -28
- package/pineforge_codegen/codegen/tv_number_format.py +9 -2
- package/pineforge_codegen/codegen/types.py +85 -4
- package/pineforge_codegen/codegen/visit_call.py +235 -55
- package/pineforge_codegen/codegen/visit_expr.py +40 -8
- package/pineforge_codegen/codegen/visit_stmt.py +5 -4
- package/pineforge_codegen/collection_history.py +122 -20
- package/pineforge_codegen/diagnostics_catalog.json +1 -0
- package/pineforge_codegen/external_requests.py +30 -9
- package/pineforge_codegen/matrix_overloads.py +7 -0
- package/pineforge_codegen/parser.py +7 -2
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +101 -10
- package/pineforge_codegen-1.4.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -3
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
693
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2285
|
-
msg_arg =
|
|
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
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
2653
|
-
self.
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
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(
|
|
2661
|
-
recv_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(
|
|
2666
|
-
|
|
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(
|
|
2786
|
+
self._type_spec_from_expr(matrix_receiver),
|
|
2676
2787
|
)
|
|
2677
2788
|
fn = MATRIX_METHODS[func_name]
|
|
2678
2789
|
try:
|
|
2679
|
-
return
|
|
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)
|
|
3574
|
-
|
|
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, (
|
|
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":
|