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
27 changes: 17 additions & 10 deletions pineforge_codegen/analyzer/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,8 +603,9 @@ def _refresh_direct_terminal_array_temporary_returns(self) -> None:
if not changed:
break

@staticmethod
@classmethod
def _direct_terminal_array_temporary_element_call(
cls,
terminal: ASTNode | None,
) -> FuncCall | None:
"""Return a direct UDF element call from the registered shape."""
Expand All @@ -624,15 +625,21 @@ def _direct_terminal_array_temporary_element_call(
else:
receiver = callee.object

while (
isinstance(receiver, FuncCall)
and isinstance(receiver.callee, MemberAccess)
and isinstance(receiver.callee.object, Identifier)
and receiver.callee.object.name == "array"
and receiver.callee.member == "copy"
and receiver.args
):
receiver = receiver.args[0]
while True:
copy_source = cls._direct_namespace_array_copy_source(receiver)
if copy_source is not None:
receiver = copy_source
continue
if (
isinstance(receiver, FuncCall)
and isinstance(receiver.callee, MemberAccess)
and receiver.callee.member == "copy"
and not receiver.args
and not receiver.kwargs
):
receiver = receiver.callee.object
continue
break
if not (
isinstance(receiver, FuncCall)
and isinstance(receiver.callee, MemberAccess)
Expand Down
79 changes: 63 additions & 16 deletions pineforge_codegen/analyzer/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,12 +325,20 @@ def terminal_expr(body):
return TypeSpec.array(self._pine_type_to_spec(first))
return TypeSpec.array(TypeSpec.primitive("float"))
# Functional-form array element/copy accessors: the receiver is
# the first argument (``array.copy(arr)``), mirroring the
# method-form handling below (``arr.copy()``).
if (ns == "array" and value.args
# the first argument (``array.copy(arr)``), or the exact ``id``
# keyword for ``array.copy(id=arr)``. The latter deliberately
# uses the shape validator shared by terminal-return recovery so
# duplicate/unknown keyword forms do not acquire a type by
# accident.
if (ns == "array"
and func in ("copy", "slice", "get", "first", "last",
"pop", "shift", "remove")):
arg_spec = self._type_spec_from_expr(value.args[0])
receiver = None
if func == "copy":
receiver = self._direct_namespace_array_copy_source(value)
elif value.args:
receiver = value.args[0]
arg_spec = self._type_spec_from_expr(receiver)
if arg_spec is not None and arg_spec.kind == "array":
if func in ("copy", "slice"):
return arg_spec
Expand Down Expand Up @@ -708,6 +716,35 @@ def _terminal_array_get_receiver(
return callee.object
return None

@staticmethod
def _direct_namespace_array_copy_source(
value: ASTNode | None,
) -> ASTNode | None:
"""Return the sole receiver of an exact ``array.copy`` call shape.

Pine v6 accepts either ``array.copy(source)`` or
``array.copy(id=source)``. Keep this structural helper exact so an
invalid duplicate receiver or an unrelated keyword stays fail closed.
Namespace shadowing is intentionally checked by the callers whose
lexical scope is still live.
"""
if not isinstance(value, FuncCall) or not isinstance(
value.callee, MemberAccess
):
return None
callee = value.callee
if not (
isinstance(callee.object, Identifier)
and callee.object.name == "array"
and callee.member == "copy"
):
return None
if len(value.args) == 1 and not value.kwargs:
return value.args[0]
if not value.args and set(value.kwargs) == {"id"}:
return value.kwargs["id"]
return None

def _is_unshadowed_direct_array_value_producer(
self,
value: ASTNode | None,
Expand All @@ -727,19 +764,22 @@ def _is_unshadowed_direct_array_value_producer(
if namespace_producer:
if callee.member != "copy":
return True
if len(value.args) != 1 or value.kwargs:
source = self._direct_namespace_array_copy_source(value)
if source is None:
return False
source = value.args[0]
if isinstance(source, Identifier):
source_spec = self._type_spec_from_expr(source)
return source_spec is not None and source_spec.kind == "array"
return self._is_unshadowed_direct_array_value_producer(source)
if callee.member != "copy" or not isinstance(
callee.object, Identifier
):
if callee.member != "copy" or value.args or value.kwargs:
return False
receiver_spec = self._type_spec_from_expr(callee.object)
return receiver_spec is not None and receiver_spec.kind == "array"
if isinstance(callee.object, Identifier):
receiver_spec = self._type_spec_from_expr(callee.object)
return receiver_spec is not None and receiver_spec.kind == "array"
# A no-argument method copy preserves the value type of an existing
# direct producer. Recurse only through that already-bounded shape;
# arbitrary UDF/slice/map receivers remain excluded.
return self._is_unshadowed_direct_array_value_producer(callee.object)

def _direct_array_value_spec_without_visiting(
self,
Expand All @@ -760,10 +800,10 @@ def _direct_array_value_spec_without_visiting(
and callee.object.name == "array"
and self._symbols.resolve("array") is None
and callee.member == "copy"
and value.args
and isinstance(value.args[0], Identifier)
):
source = value.args[0]
candidate = self._direct_namespace_array_copy_source(value)
if isinstance(candidate, Identifier):
source = candidate
elif callee.member == "copy" and isinstance(
callee.object, Identifier
):
Expand Down Expand Up @@ -832,6 +872,11 @@ def _cached_direct_array_value_spec(
):
return None
callee = value.callee
if callee.member == "copy" and not value.args and not value.kwargs:
# Method syntax carries its source in the callee object rather
# than in ``args``. Peel only a direct producer and stay entirely
# on cached metadata so a stateful element call is never revisited.
return self._cached_direct_array_value_spec(callee.object)
if not (
isinstance(callee.object, Identifier)
and callee.object.name == "array"
Expand Down Expand Up @@ -859,8 +904,10 @@ def _cached_direct_array_value_spec(
if producer == "from" and value.args:
element = self._cached_primitive_expr_spec(value.args[0])
return TypeSpec.array(element) if element is not None else None
if producer == "copy" and value.args:
return self._cached_direct_array_value_spec(value.args[0])
if producer == "copy":
source = self._direct_namespace_array_copy_source(value)
if source is not None:
return self._cached_direct_array_value_spec(source)
return None

def _cached_terminal_temporary_array_get_spec(
Expand Down
6 changes: 4 additions & 2 deletions pineforge_codegen/codegen/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,9 +569,11 @@ def terminal_expr(body):
recv_spec = self._type_spec_from_expr(node.callee.object)
member_name = node.callee.member
if recv_spec is not None and recv_spec.kind == "array":
if func_name in ("get", "first", "last", "pop", "shift", "remove"):
if member_name in (
"get", "first", "last", "pop", "shift", "remove",
):
return recv_spec.element
if func_name in ("copy", "slice"):
if member_name in ("copy", "slice"):
return recv_spec
if recv_spec is not None and recv_spec.kind == "map":
if member_name in ("put", "get", "remove"):
Expand Down
14 changes: 13 additions & 1 deletion pineforge_codegen/codegen/visit_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,16 @@ def _array_method_arg_nodes(self, method: str, node: FuncCall) -> list:

def _array_function_arg_nodes(self, method: str, node: FuncCall) -> list:
"""Merge ``array.method(id=..., ...)`` arguments in signature order."""
if method == "copy":
if len(node.args) == 1 and not node.kwargs:
return list(node.args)
if not node.args and set(node.kwargs) == {"id"}:
return [node.kwargs["id"]]
self._codegen_error(
node,
"array.copy: expected exactly one receiver 'id'",
hint="Use array.copy(source) or array.copy(id=source).",
)
param_names = CHECKED_ARRAY_METHOD_KWARGS.get(method)
if param_names is None:
return list(node.args)
Expand Down Expand Up @@ -1239,7 +1249,9 @@ 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 or node.kwargs):
if func_name in ARRAY_METHODS and (
node.args or node.kwargs or func_name == "copy"
):
all_nodes = self._array_function_arg_nodes(func_name, node)
if not all_nodes:
return "0"
Expand Down
Loading
Loading