From 77759b030aad92cbc6e2ef6fb9ab639ccd005268 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 18 Jul 2026 05:17:10 +0800 Subject: [PATCH] fix(codegen): route array parameter methods by type --- pineforge_codegen/codegen/types.py | 4 +- pineforge_codegen/codegen/visit_call.py | 21 +++ tests/test_array_checked_access.py | 194 ++++++++++++++++++++++++ tests/test_compile_smoke.py | 29 ++++ 4 files changed, 246 insertions(+), 2 deletions(-) diff --git a/pineforge_codegen/codegen/types.py b/pineforge_codegen/codegen/types.py index ddfe0d3..35516ee 100644 --- a/pineforge_codegen/codegen/types.py +++ b/pineforge_codegen/codegen/types.py @@ -213,11 +213,11 @@ def _type_spec_from_expr(self, node) -> TypeSpec | None: loop_specs = getattr(self, "_current_loop_var_specs", None) if loop_specs and node.name in loop_specs: return loop_specs[node.name] - if node.name in self._collection_types: - return self._collection_types[node.name] param_specs = getattr(self, "_current_func_param_specs", {}) if node.name in param_specs: return param_specs[node.name] + if node.name in self._collection_types: + return self._collection_types[node.name] if node.name in self._udt_var_types: return TypeSpec.udt(self._udt_var_types[node.name]) # Drawing-typed method/function parameter (L.6d / U.5): a ``line ln`` diff --git a/pineforge_codegen/codegen/visit_call.py b/pineforge_codegen/codegen/visit_call.py index f2264f4..c58f5e3 100644 --- a/pineforge_codegen/codegen/visit_call.py +++ b/pineforge_codegen/codegen/visit_call.py @@ -397,6 +397,27 @@ def _visit_func_call(self, node: FuncCall) -> str: or oname in self._global_member_vars ): meth_raw = callee.member + # Function parameters have lexical precedence over the + # global collection registries. Route a declared/inferred + # ``array`` parameter before consulting same-named + # global map/array/matrix entries, and retain its element + # TypeSpec for checked reads and mutations. + param_spec = getattr( + self, "_current_func_param_specs", {} + ).get(oname) + if ( + param_spec is not None + and param_spec.kind == "array" + 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, arg_nodes, param_spec + ) + return self._array_method_expr( + arr, meth_raw, margs, param_spec + ) # map.put / map.get / … must lower to unordered_map ops, not `.put` on C++ if oname in self._map_vars and meth_raw in MAP_METHODS: m = self._safe_name(oname) diff --git a/tests/test_array_checked_access.py b/tests/test_array_checked_access.py index ec3c3c7..bde01be 100644 --- a/tests/test_array_checked_access.py +++ b/tests/test_array_checked_access.py @@ -343,6 +343,79 @@ def test_checked_udt_lvalue_access_preserves_alias(access: str): """ +_TYPED_ARRAY_PARAM_METHOD_SOURCE = """//@version=6 +strategy("Typed array parameter methods") +type Pivot + float level +var order = array.new() +var array shadow_array = array.from(99.0) +var map shadow_map = map.new() +var matrix shadow_matrix = matrix.new(1, 1, 99.0) + +next_index() => + order.push(1) + 0 + +next_value() => + order.push(2) + 9 + +rank_pos(array source, int index) => + source.percentrank(index) + +rank_kw(array source, int index) => + source.percentrank(index=index) + +mutate_int(array source) => + source.set(index=next_index(), value=next_value()) + source.push(10) + source.get(index=0) + +probe_bool(array source) => + bool before = source.first() + source.set(0, false) + source.push(true) + before and source.last() and not source.get(0) + +mutate_pivot(array source) => + Pivot p = source.get(0) + p.level := 7 + p.level + +shadow_array_probe(array shadow_array) => + array copied = shadow_array.copy() + shadow_array.push("tail") + copied.size() + +shadow_map_rank(array shadow_map, int index) => + shadow_map.percentrank(index) + +shadow_matrix_rank(array shadow_matrix, int index) => + shadow_matrix.percentrank(index=index) + +float_values = array.from(1.0, 2.0, 3.0) +int_values = array.from(1, 2) +bool_values = array.from(true) +pivot_values = array.from(Pivot.new(1)) +string_values = array.from("head") +rank_pos_result = rank_pos(float_values, 2) +rank_kw_result = rank_kw(float_values, 2) +int_result = mutate_int(int_values) +bool_result = probe_bool(bool_values) +pivot_result = mutate_pivot(pivot_values) +shadow_array_copy_size = shadow_array_probe(string_values) +shadow_map_result = shadow_map_rank(float_values, 2) +shadow_matrix_result = shadow_matrix_rank(float_values, 2) +int_caller_value = int_values.get(0) +int_caller_size = int_values.size() +bool_caller_value = bool_values.get(0) +bool_caller_size = bool_values.size() +pivot_caller_value = pivot_values.get(0).level +string_caller_size = string_values.size() +order_code = order.get(0) * 10 + order.get(1) +""" + + _PERCENTRANK_BOUNDS_ERROR_SOURCE = """//@version=6 strategy("PercentRank bounds errors") selector = close @@ -797,3 +870,124 @@ def test_oob_and_empty_methods_surface_deterministic_last_error(): 12: "Index na is out of bounds. Array size is 3", 13: "Index inf is out of bounds. Array size is 3", } + + +def test_array_typed_udf_parameter_methods_route_by_typespec(): + cpp = transpile(_TYPED_ARRAY_PARAM_METHOD_SOURCE) + + # Declared parameter element types survive into both the C++ reference + # signature and method lowering (including vector's proxy type and a + # user-defined element type). + assert "double rank_pos(std::vector& source, int index)" in cpp + assert "double rank_kw(std::vector& source, int index)" in cpp + assert "double mutate_int(std::vector& source)" in cpp + assert "bool probe_bool(std::vector& source)" in cpp + assert "double mutate_pivot(std::vector& source)" in cpp + assert ( + "double shadow_array_probe(std::vector& shadow_array)" + in cpp + ) + assert ( + "double shadow_map_rank(std::vector& shadow_map, int index)" + in cpp + ) + assert ( + "double shadow_matrix_rank(std::vector& shadow_matrix, int index)" + in cpp + ) + assert ( + "std::vector copied = " + "std::vector(shadow_array);" + ) in cpp + + # Both positional and keyword PercentRank forms use the non-normalizing, + # checked lowering. The remaining checked reads use the separate v6 + # negative-index normalization expression. + assert cpp.count("int64_t __pf_array_index=__pf_raw_index;") == 4 + assert "int64_t __pf_array_index=__pf_raw_index<0?" in cpp + assert cpp.count("pine_runtime_error") >= 2 + + # Read and mutation methods no longer leak as raw, invalid C++ member + # calls. Mutation still targets the reference parameter directly. + for raw_call in ( + "source.percentrank(", + "source.set(", + "source.push(", + "source.get(", + "source.first(", + "source.last(", + "shadow_array.push(", + "shadow_array.copy(", + "shadow_map.percentrank(", + "shadow_matrix.percentrank(", + ): + assert raw_call not in cpp + assert "source.push_back(10);" in cpp + assert "source.push_back(true);" in cpp + assert re.search(r"Pivot& p = .*pine_runtime_error", cpp) + + # The checked set wrapper evaluates index then value exactly once. Its + # nested-call spelling closes in reverse textual order, locking execution + # order the same way as the namespace/global/local receiver lanes. + statement = next( + line for line in cpp.splitlines() + if "next_index()" in line and "next_value()" in line + ) + assert statement.count("next_index()") == 1 + assert statement.count("next_value()") == 1 + assert statement.index("[&](auto&& __pf_array)") < statement.index( + "[&](auto&& __pf_raw_index_value)" + ) < statement.index("[&](auto&& __pf_array_value)") + assert statement.index("}((next_value()))") < statement.index( + "}((next_index()))" + ) + + +def test_array_typed_udf_parameter_methods_preserve_runtime_aliases(): + driver = r""" +#include +int main() { + GeneratedStrategy strategy; + Bar bar{1.0, 1.0, 1.0, 1.0, 1.0, 0}; + strategy.run(&bar, 1); + if (!strategy.last_error().empty()) { + std::cerr << strategy.last_error() << "\n"; + return 2; + } + std::cout << strategy.rank_pos_result << " " + << strategy.rank_kw_result << " " + << strategy.int_result << " " + << strategy.int_caller_value << " " + << strategy.int_caller_size << " " + << strategy.bool_result << " " + << strategy.bool_caller_value << " " + << strategy.bool_caller_size << " " + << strategy.pivot_result << " " + << strategy.pivot_caller_value << " " + << strategy.shadow_map_result << " " + << strategy.shadow_matrix_result << " " + << strategy.string_caller_size << " " + << strategy.shadow_array_copy_size << " " + << strategy.order_code << "\n"; +} +""" + output = _compile_and_run( + transpile(_TYPED_ARRAY_PARAM_METHOD_SOURCE) + driver + ) + assert tuple(int(float(value)) for value in output.split()) == ( + 100, + 100, + 9, + 9, + 3, + 1, + 0, + 2, + 7, + 7, + 100, + 100, + 2, + 1, + 12, + ) diff --git a/tests/test_compile_smoke.py b/tests/test_compile_smoke.py index 5477abc..ba1c4b2 100644 --- a/tests/test_compile_smoke.py +++ b/tests/test_compile_smoke.py @@ -410,6 +410,35 @@ def test_arrays_compile(): """) +def test_array_typed_udf_parameter_methods_compile(): + _check("array_typed_udf_parameter_methods", """ +type Pivot + float level +rank(array values, int index) => + values.percentrank(index=index) +mutate(array values) => + values.set(index=0, value=9) + values.push(10) + values.get(index=0) +flip(array values) => + values.set(0, false) + values.push(true) + values.last() +mutate_pivot(array values) => + Pivot p = values.get(0) + p.level := 7 + p.level +floats = array.from(1.0, 2.0, 3.0) +ints = array.from(1, 2) +flags = array.from(true) +pivots = array.from(Pivot.new(1)) +rank_result = rank(floats, 2) +int_result = mutate(ints) +bool_result = flip(flags) +pivot_result = mutate_pivot(pivots) +""") + + def test_nested_array_slice_aggregates_compile(): _check("nested_array_slice_aggregates", """ a = array.from(1.0, 3.0, 2.0)