Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 128 additions & 7 deletions pineforge_codegen/codegen/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,18 +458,126 @@ def tz_time_field_lambda(field_expr: str, ts_arg: str, tz_arg: str) -> str:
# Array / Map / Matrix method dispatch
# ---------------------------------------------------------------------------


def _checked_array_index_prelude() -> str:
"""C++ body shared by Pine v6 checked single-index operations."""
return (
"using __pf_raw_index_type=std::decay_t<decltype(__pf_raw_index_value)>; "
"if constexpr(!std::is_same_v<__pf_raw_index_type,bool>) { "
"if(is_na(__pf_raw_index_value)) "
"pine_runtime_error(std::string(\"Index na is out of bounds. Array size is \")+"
"std::to_string((int64_t)__pf_array.size())); } "
"if constexpr(std::is_floating_point_v<__pf_raw_index_type>) { "
"if(!std::isfinite(__pf_raw_index_value)) { "
"std::string __pf_raw_index_text=__pf_raw_index_value>0?\"inf\":\"-inf\"; "
"pine_runtime_error(std::string(\"Index \")+__pf_raw_index_text+"
"\" is out of bounds. Array size is \"+"
"std::to_string((int64_t)__pf_array.size())); } "
"long double __pf_raw_index_wide=(long double)__pf_raw_index_value; "
"if(__pf_raw_index_wide<(long double)std::numeric_limits<int64_t>::min()||"
"__pf_raw_index_wide>(long double)std::numeric_limits<int64_t>::max()) "
"pine_runtime_error(std::string(\"Index \")+"
"std::to_string((double)__pf_raw_index_value)+"
"\" is out of bounds. Array size is \"+"
"std::to_string((int64_t)__pf_array.size())); } "
"int64_t __pf_raw_index=(int64_t)__pf_raw_index_value; "
"int64_t __pf_array_size=(int64_t)__pf_array.size(); "
"int64_t __pf_array_index=__pf_raw_index<0?"
"__pf_raw_index+__pf_array_size:__pf_raw_index; "
"if(__pf_array_index<0||__pf_array_index>=__pf_array_size) "
"pine_runtime_error(std::string(\"Index \")+std::to_string(__pf_raw_index)+"
"\" is out of bounds. Array size is \"+std::to_string(__pf_array_size)); "
)


def _checked_array_get(a: str, args: list[str]) -> str:
check = _checked_array_index_prelude()
return (
"[&](auto&& __pf_array)->decltype(auto){ "
"return [&](auto&& __pf_raw_index_value)->decltype(auto){ "
f"{check}"
"if constexpr(std::is_lvalue_reference_v<decltype(__pf_array)>) "
"return (__pf_array[(size_t)__pf_array_index]); "
"else { using __pf_array_value_type=typename "
"std::decay_t<decltype(__pf_array)>::value_type; "
"return __pf_array_value_type(__pf_array[(size_t)__pf_array_index]); } "
f"}}(({args[0]})); }}(({a}))"
)


def _checked_array_set(a: str, args: list[str]) -> str:
check = _checked_array_index_prelude()
return (
"[&](auto&& __pf_array){ "
"return [&](auto&& __pf_raw_index_value){ "
"return [&](auto&& __pf_array_value){ "
f"{check}"
"__pf_array[(size_t)__pf_array_index]=__pf_array_value; "
f"}}(({args[1]})); }}(({args[0]})); }}(({a}))"
)


def _checked_array_remove(a: str, args: list[str]) -> str:
check = _checked_array_index_prelude()
return (
"[&](auto&& __pf_array){ "
"return [&](auto&& __pf_raw_index_value){ "
f"{check}"
"using __pf_array_value_type=typename "
"std::decay_t<decltype(__pf_array)>::value_type; "
"__pf_array_value_type __pf_array_value="
"__pf_array[(size_t)__pf_array_index]; "
"__pf_array.erase(__pf_array.begin()+(size_t)__pf_array_index); "
"return __pf_array_value; "
f"}}(({args[0]})); }}(({a}))"
)


def _checked_array_end_get(a: str, method: str) -> str:
access = "front" if method == "first" else "back"
return (
"[&](auto&& __pf_array)->decltype(auto){ "
f"if(__pf_array.empty()) pine_runtime_error(\"Cannot use {method}() "
"if array is empty.\"); "
"if constexpr(std::is_lvalue_reference_v<decltype(__pf_array)>) "
f"return (__pf_array.{access}()); "
"else { using __pf_array_value_type=typename "
"std::decay_t<decltype(__pf_array)>::value_type; "
f"return __pf_array_value_type(__pf_array.{access}()); }} "
f"}}(({a}))"
)


def _checked_array_end_remove(a: str, method: str) -> str:
access = "back" if method == "pop" else "front"
mutation = (
"__pf_array.pop_back();"
if method == "pop"
else "__pf_array.erase(__pf_array.begin());"
)
return (
"[&](auto&& __pf_array){ "
f"if(__pf_array.empty()) pine_runtime_error(\"Cannot use {method}() "
"if array is empty.\"); "
"using __pf_array_value_type=typename "
"std::decay_t<decltype(__pf_array)>::value_type; "
f"__pf_array_value_type __pf_array_value=__pf_array.{access}(); "
f"{mutation} return __pf_array_value; }}(({a}))"
)


# Methods called as ``array.method(arr, ...)`` or ``arr.method(...)``.
ARRAY_METHODS = {
"get": lambda a, args: f"{a}[({args[0]})]",
"set": lambda a, args: f"{a}[({args[0]})] = {args[1]}",
"get": _checked_array_get,
"set": _checked_array_set,
"push": lambda a, args: f"{a}.push_back({args[0]})",
"unshift": lambda a, args: f"{a}.insert({a}.begin(), {args[0]})",
"insert": lambda a, args: f"{a}.insert({a}.begin() + (int)({args[0]}), {args[1]})",
"pop": lambda a, args: f"[&](){{ auto v={a}.back(); {a}.pop_back(); return v; }}()",
"shift": lambda a, args: f"[&](){{ auto v={a}.front(); {a}.erase({a}.begin()); return v; }}()",
"remove": lambda a, args: f"[&](){{ auto v={a}[({args[0]})]; {a}.erase({a}.begin()+(int)({args[0]})); return v; }}()",
"first": lambda a, args: f"{a}.front()",
"last": lambda a, args: f"{a}.back()",
"pop": lambda a, args: _checked_array_end_remove(a, "pop"),
"shift": lambda a, args: _checked_array_end_remove(a, "shift"),
"remove": _checked_array_remove,
"first": lambda a, args: _checked_array_end_get(a, "first"),
"last": lambda a, args: _checked_array_end_get(a, "last"),
"size": lambda a, args: f"(double){a}.size()",
"clear": lambda a, args: f"{a}.clear()",
"fill": lambda a, args: f"std::fill({a}.begin(), {a}.end(), {args[0]})" if len(args) == 1
Expand Down Expand Up @@ -523,6 +631,19 @@ def tz_time_field_lambda(field_expr: str, ts_arg: str, tz_arg: str) -> str:
"sort_indices": lambda a, args: f"[&](){{ std::vector<double> idx({a}.size()); std::iota(idx.begin(),idx.end(),0); std::sort(idx.begin(),idx.end(),[&](int i,int j){{return {a}[i]<{a}[j];}}); return idx; }}()",
}

# Pine parameter order for the checked-access subset. Keeping the receiver
# (``id``) separate lets method syntax merge ``a.get(index = i)`` while the
# namespace-functional path merges ``array.get(id = a, index = i)``.
CHECKED_ARRAY_METHOD_KWARGS: dict[str, list[str]] = {
"get": ["index"],
"set": ["index", "value"],
"remove": ["index"],
"first": [],
"last": [],
"pop": [],
"shift": [],
}

MAP_METHODS = {
"put": lambda m, args: f"({m}[{args[0]}] = {args[1]})",
"get": lambda m, args: f"({m}.count({args[0]}) ? {m}[{args[0]}] : na<double>())",
Expand Down
62 changes: 58 additions & 4 deletions pineforge_codegen/codegen/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,24 @@ def _type_spec_from_expr(self, node) -> TypeSpec | None:
if sym is not None and getattr(sym, "type_spec", None) is not None:
return sym.type_spec
return None
if isinstance(node, Ternary):
true_spec = self._type_spec_from_expr(node.true_val)
false_spec = self._type_spec_from_expr(node.false_val)
if true_spec is not None and true_spec == false_spec:
return true_spec
return None
if isinstance(node, MemberAccess):
owner = self._type_spec_from_expr(node.object)
if owner is not None and owner.kind == "udt" and owner.name:
return (self._udt_field_type_specs.get(owner.name) or {}).get(node.member)
return None
if isinstance(node, FuncCall):
func_name, namespace = self._resolve_callee(node.callee)
if namespace is None:
func_info = self._func_info_map.get(func_name)
return_spec = getattr(func_info, "return_type_spec", None)
if return_spec is not None:
return return_spec
# ticker.* constructors (inherit/standard/heikinashi) return a symbol
# string; without this the member-type inference defaults to double
# and a ``haTicker = ticker.heikinashi(...)`` global mis-declares as
Expand Down Expand Up @@ -278,10 +289,11 @@ def _type_spec_from_expr(self, node) -> TypeSpec | None:
# Functional-form array element/copy accessors: the receiver is
# the first argument (``array.copy(arr)``), mirroring the
# method-form handling below (``arr.copy()``).
if (namespace == "array" and node.args
if (namespace == "array"
and func_name in ("copy", "slice", "get", "first", "last",
"pop", "shift", "remove")):
arg_spec = self._type_spec_from_expr(node.args[0])
receiver_node = node.args[0] if node.args else node.kwargs.get("id")
arg_spec = self._type_spec_from_expr(receiver_node)
if arg_spec is not None and arg_spec.kind == "array":
if func_name in ("copy", "slice"):
return arg_spec
Expand Down Expand Up @@ -646,6 +658,46 @@ def _na_reassign_cpp_type(self, name: str) -> str | None:
# BUG C: user-defined-UDT lvalue aliasing
# ------------------------------------------------------------------

def _is_stable_lvalue_expr(self, expr) -> bool:
"""Whether ``expr`` denotes storage that can safely back a C++ ref.

Checked array access deliberately returns by reference for lvalue
receivers and by value for temporary receivers. The UDT alias pass
must make the same distinction or it can emit ``T&`` bound to the
checked helper's safe rvalue copy.
"""
if isinstance(expr, Identifier):
return True
if isinstance(expr, MemberAccess):
return self._is_stable_lvalue_expr(expr.object)
if isinstance(expr, FuncCall):
# An element selected from a stable array is itself stable. This
# must recurse independently of the element type so nested
# ``array<array<UDT>>`` access keeps the inner array lvalue and the
# eventual UDT element can still alias it. Constructors, user
# function returns, matrix.row(), and other temporary producers do
# not enter this checked array-access shape.
func_name, namespace = self._resolve_callee(expr.callee)
receiver = None
if namespace == "array" and func_name in ("get", "first", "last"):
receiver = expr.args[0] if expr.args else expr.kwargs.get("id")
elif (isinstance(expr.callee, MemberAccess)
and func_name in ("get", "first", "last")):
candidate = expr.callee.object
candidate_spec = self._type_spec_from_expr(candidate)
if candidate_spec is not None and candidate_spec.kind == "array":
receiver = candidate
return (
receiver is not None
and self._is_stable_lvalue_expr(receiver)
)
if isinstance(expr, Ternary):
return (
self._is_stable_lvalue_expr(expr.true_val)
and self._is_stable_lvalue_expr(expr.false_val)
)
return False

def _is_udt_lvalue(self, expr) -> str | None:
"""If ``expr`` is a *user-defined* UDT lvalue (a bare ``Identifier`` that
names a class-scope ``var``/global UDT member, e.g. ``wyckoffSwingLow``,
Expand All @@ -659,12 +711,14 @@ def _is_udt_lvalue(self, expr) -> str | None:
callee = expr.callee
func_name, namespace = self._resolve_callee(callee)
receiver = None
if namespace == "array" and func_name in ("get", "first", "last") and expr.args:
receiver = expr.args[0]
if namespace == "array" and func_name in ("get", "first", "last"):
receiver = expr.args[0] if expr.args else expr.kwargs.get("id")
elif (isinstance(callee, MemberAccess)
and func_name in ("get", "first", "last")):
receiver = callee.object
if receiver is not None:
if not self._is_stable_lvalue_expr(receiver):
return None
spec = self._type_spec_from_expr(receiver)
elem = spec.element if spec is not None and spec.kind == "array" else None
if (elem is not None and elem.kind == "udt" and elem.name in self._udt_defs
Expand Down
55 changes: 48 additions & 7 deletions pineforge_codegen/codegen/visit_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
ARRAY_METHODS,
BAR_FIELDS,
BAR_SERIES_PUSH,
CHECKED_ARRAY_METHOD_KWARGS,
DRAWING_NS,
DRAWING_TYPE_TO_CPP,
MAP_METHODS,
Expand Down Expand Up @@ -281,6 +282,22 @@ def _array_method_args(
for idx, arg in enumerate(arg_nodes)
]

def _array_method_arg_nodes(self, method: str, node: FuncCall) -> list:
"""Merge checked-array method kwargs into Pine signature order."""
param_names = CHECKED_ARRAY_METHOD_KWARGS.get(method)
if param_names is None:
return list(node.args)
return _merge_kwargs(node.args, node.kwargs, param_names, lambda arg: arg)

def _array_function_arg_nodes(self, method: str, node: FuncCall) -> list:
"""Merge ``array.method(id=..., ...)`` arguments in signature order."""
param_names = CHECKED_ARRAY_METHOD_KWARGS.get(method)
if param_names is None:
return list(node.args)
return _merge_kwargs(
node.args, node.kwargs, ["id", *param_names], lambda arg: arg
)

def _visit_func_call(self, node: FuncCall) -> str:
callee = node.callee
if isinstance(callee, MemberAccess):
Expand Down Expand Up @@ -320,6 +337,21 @@ def _visit_func_call(self, node: FuncCall) -> str:
list(node.args), node,
)

# Array method on an arbitrary expression receiver, e.g.
# ``make_array().get(-1)``, ``m.row(0).last()``, or
# ``(cond ? a : b).get(-1)``. Identifier and member receivers have
# dedicated paths below; other receiver shapes used to fall through to
# ``None(...)`` despite carrying a known array spec.
if (isinstance(callee, MemberAccess)
and not isinstance(callee.object, (Identifier, MemberAccess))
and callee.member in ARRAY_METHODS):
recv_spec = self._type_spec_from_expr(callee.object)
if recv_spec is not None and recv_spec.kind == "array":
recv = self._visit_expr(callee.object)
arg_nodes = self._array_method_arg_nodes(callee.member, node)
args = self._array_method_args(callee.member, arg_nodes, recv_spec)
return self._array_method_expr(recv, callee.member, args, recv_spec)

# chart.point.now/new/from_index/from_time/copy — REAL data (a ChartPoint
# aggregate). Routed here BEFORE the obj.field.method receiver logic,
# which would otherwise mis-treat ``chart.point`` as a receiver object
Expand All @@ -342,8 +374,12 @@ def _visit_func_call(self, node: FuncCall) -> str:
meth = callee.member
raw_args = [self._visit_expr(a) for a in node.args]
if recv_spec is not None and recv_spec.kind == "array" and meth in ARRAY_METHODS:
arg_nodes = self._array_method_arg_nodes(meth, node)
return self._array_method_expr(
recv, meth, self._array_method_args(meth, node.args, recv_spec), recv_spec
recv,
meth,
self._array_method_args(meth, arg_nodes, recv_spec),
recv_spec,
)
if recv_spec is not None and recv_spec.kind == "map" and meth in MAP_METHODS:
return self._map_method_expr(recv, meth, raw_args, recv_spec)
Expand All @@ -368,8 +404,9 @@ def _visit_func_call(self, node: FuncCall) -> str:
return self._map_method_expr(m, meth_raw, margs, self._map_spec_for_name(oname))
if oname in self._array_vars and meth_raw in ARRAY_METHODS:
arr = self._safe_name(oname)
arg_nodes = self._array_method_arg_nodes(meth_raw, node)
margs = self._array_method_args(
meth_raw, node.args, self._array_spec_for_name(oname)
meth_raw, arg_nodes, self._array_spec_for_name(oname)
)
return self._array_method_expr(arr, meth_raw, margs, self._array_spec_for_name(oname))
if oname in self._matrix_specs and meth_raw in MATRIX_METHODS:
Expand Down Expand Up @@ -538,7 +575,8 @@ def _visit_func_call(self, node: FuncCall) -> str:
if namespace is not None and namespace in self._array_vars and func_name in ARRAY_METHODS:
arr = self._safe_name(namespace)
spec = self._array_spec_for_name(namespace)
args = self._array_method_args(func_name, node.args, spec)
arg_nodes = self._array_method_arg_nodes(func_name, node)
args = self._array_method_args(func_name, arg_nodes, spec)
return self._array_method_expr(arr, func_name, args, spec)

# Array operations — emit proper C++ vector operations
Expand All @@ -561,10 +599,13 @@ def _visit_func_call(self, node: FuncCall) -> str:
elems = ", ".join(self._visit_expr(a) for a in node.args)
return f"{self._type_spec_to_cpp(spec)}{{{elems}}}"
# Method calls: array.method(arr, args...)
if func_name in ARRAY_METHODS and node.args:
arr = self._visit_expr(node.args[0])
spec = self._type_spec_from_expr(node.args[0])
rest = self._array_method_args(func_name, node.args[1:], spec)
if func_name in ARRAY_METHODS and (node.args or node.kwargs):
all_nodes = self._array_function_arg_nodes(func_name, node)
if not all_nodes:
return "0"
arr = self._visit_expr(all_nodes[0])
spec = self._type_spec_from_expr(all_nodes[0])
rest = self._array_method_args(func_name, all_nodes[1:], spec)
return self._array_method_expr(arr, func_name, rest, spec)
return "0"

Expand Down
Loading
Loading