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
15 changes: 15 additions & 0 deletions pineforge_codegen/analyzer/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1989,6 +1989,13 @@ def _visit_FuncDef(self, node: FuncDef) -> PineType:
for name, spec in zip(node.params, inferred_param_specs)
},
)
terminal_array_get_return = self._terminal_array_get_return(
terminal_ret_expr,
{
name: spec
for name, spec in zip(node.params, inferred_param_specs)
},
)

self._symbols.exit_scope()

Expand Down Expand Up @@ -2049,6 +2056,14 @@ def _visit_FuncDef(self, node: FuncDef) -> PineType:
if terminal_spec is not None:
self._func_return_type_specs[node.name] = terminal_spec

if terminal_array_get_return is not None:
body_type, terminal_spec = terminal_array_get_return
# Preserve the exact primitive TypeSpec as well as the coarse
# PineType. Downstream collection construction consults this cache
# directly; without it, asking for the UDF's element type can
# re-visit the call and duplicate stateful call sites.
self._func_return_type_specs[node.name] = terminal_spec

# Store return type
self._func_return_types[node.name] = body_type

Expand Down
13 changes: 7 additions & 6 deletions pineforge_codegen/analyzer/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,12 @@ class FuncInfo:
# ``pivot hi`` parameter emits as ``pivot hi`` (not ``double hi``) and an
# untyped ``s`` used as a string emits as ``std::string s``.
param_type_specs: list = field(default_factory=list)
# ``TypeSpec`` of the function's return value when it is a collection the
# coarse ``return_type`` (PineType) cannot represent — arrays and maps
# (e.g. ``buildPDLevels() => array.from(...)`` -> ``std::vector<double>``).
# UDT / drawing-handle returns use
# ``udt_return_type``; tuple returns use ``returns_tuple``.
# Exact ``TypeSpec`` of the function's return value when downstream
# expression inference needs more than the coarse ``return_type`` slot.
# This primarily carries arrays/maps and also direct-terminal primitive
# array-element reads, whose cached spec prevents inference from re-visiting
# a stateful UDF call. UDT / drawing-handle returns use ``udt_return_type``;
# tuple returns use ``returns_tuple``.
return_type_spec: Any = None


Expand Down Expand Up @@ -249,7 +250,7 @@ class AnalyzerContext:
# Per-function var_members + series_vars (used when emitting per-function call-site variants):
func_var_members: dict = field(default_factory=dict)
func_series_vars: dict = field(default_factory=dict)
# Per-function collection-return TypeSpec (see FuncInfo.return_type_spec).
# Per-function exact return TypeSpec (see FuncInfo.return_type_spec).
func_return_type_specs: dict = field(default_factory=dict)
# var_name -> UDT type name for variables instantiated via TypeName.new(...)
udt_var_types: dict[str, str] = field(default_factory=dict)
Expand Down
108 changes: 108 additions & 0 deletions pineforge_codegen/analyzer/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,114 @@ def _terminal_map_call_return(
return PineType.STRING, None
return None

def _terminal_array_get_return(
self,
value: ASTNode | None,
parameter_specs: dict[str, TypeSpec | None] | None = None,
) -> tuple[PineType, TypeSpec] | None:
"""Return exact metadata for a direct terminal ``array.get`` call.

The regular expression ``TypeSpec`` pass already knows the element
type of both ``array.get(values, index)`` and ``values.get(index)``.
The coarse visitor, however, deliberately returns ``VOID`` for most
array methods. When such a call is the final expression of a UDF,
that ``VOID`` used to make codegen emit a ``double`` return type even
for primitive elements such as strings, booleans, and integers.

Keep the refinement intentionally narrow: only the established
positional/keyword shapes of ``get`` participate, and the receiver
must resolve to an exact array ``TypeSpec``. Other array accessors,
mutations, range/view semantics, reference/ID-like elements, and
unresolved receivers remain on their existing paths. Returning UDTs
or nested collections by value would lose Pine reference identity even
when the generated C++ happens to compile, so this helper must not
expose those specs as an apparent fix.
"""
if not isinstance(value, FuncCall) or not isinstance(
value.callee, MemberAccess
):
return None

callee = value.callee
if callee.member != "get":
return None

receiver: ASTNode | None = None
object_spec = None
object_is_parameter = False
object_is_visible_binding = False
if isinstance(callee.object, Identifier):
object_is_visible_binding = (
self._symbols.resolve(callee.object.name) is not None
)
object_is_parameter = (
parameter_specs is not None
and callee.object.name in parameter_specs
)
if object_is_parameter:
object_spec = parameter_specs.get(callee.object.name)
else:
object_spec = self._type_spec_from_expr(callee.object)
object_is_array_value = (
object_spec is not None and object_spec.kind == "array"
)
if (
isinstance(callee.object, Identifier)
and callee.object.name == "array"
and not object_is_array_value
and not object_is_visible_binding
):
# Functional forms supported by the checked-array emitter:
# array.get(values, index)
# array.get(values, index=index)
# array.get(id=values, index=index)
if len(value.args) == 2 and not value.kwargs:
receiver = value.args[0]
elif len(value.args) == 1 and set(value.kwargs) == {"index"}:
receiver = value.args[0]
elif not value.args and set(value.kwargs) == {"id", "index"}:
receiver = value.kwargs["id"]
else:
# Method forms supported by the checked-array emitter:
# values.get(index)
# values.get(index=index)
if len(value.args) == 1 and not value.kwargs:
receiver = callee.object
elif not value.args and set(value.kwargs) == {"index"}:
receiver = callee.object

if receiver is None:
return None
# Temporary/arbitrary receivers have their own stateful-call and
# reference-identity questions. Re-inferring them here would revisit
# nested calls and mint extra call-site state. The registered residual
# is limited to exact identifier receivers.
if not isinstance(receiver, Identifier):
return None

recv_spec = None
if parameter_specs is not None and receiver.name in parameter_specs:
recv_spec = parameter_specs.get(receiver.name)
# An unresolved parameter shadows any same-named global.
if recv_spec is None:
return None
else:
recv_spec = self._type_spec_from_expr(receiver)
if (
recv_spec is None
or recv_spec.kind != "array"
or recv_spec.element is None
):
return None

element_spec = recv_spec.element
if element_spec.kind != "primitive":
return None
return_type = self._element_pine_type(element_spec)
if return_type == PineType.VOID:
return None
return return_type, element_spec

@staticmethod
def _pine_type_to_spec(pine_type: PineType) -> TypeSpec:
mapping = {
Expand Down
4 changes: 2 additions & 2 deletions pineforge_codegen/codegen/emit_top.py
Original file line number Diff line number Diff line change
Expand Up @@ -1303,8 +1303,8 @@ def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | No
# struct (Line/Box/Label/Linefill), not the unknown lowercase name.
ret_type = DRAWING_TYPE_TO_CPP.get(fi.udt_return_type, fi.udt_return_type)
elif getattr(fi, "return_type_spec", None) is not None:
# Collection-returning function (array or map terminal) — emit the
# concrete C++ collection type from its inferred TypeSpec.
# Exact return TypeSpec (collections plus narrowly cached primitive
# terminal reads) takes precedence over the coarse PineType slot.
ret_type = self._type_spec_to_cpp(fi.return_type_spec)
else:
ret_type = PINE_TYPE_TO_CPP.get(fi.return_type, "double")
Expand Down
2 changes: 1 addition & 1 deletion tests/test_array_checked_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,7 @@ def test_array_typed_udf_parameter_methods_route_by_typespec():
# user-defined element type).
assert "double rank_pos(std::vector<double>& source, int index)" in cpp
assert "double rank_kw(std::vector<double>& source, int index)" in cpp
assert "double mutate_int(std::vector<int>& source)" in cpp
assert "int mutate_int(std::vector<int>& source)" in cpp
assert "bool probe_bool(std::vector<bool>& source)" in cpp
assert "double mutate_pivot(std::vector<Pivot>& source)" in cpp
assert (
Expand Down
Loading
Loading