From 55ef735b11cd299c56213d28355e13225f935e7d Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Tue, 21 Jul 2026 22:16:49 +0800 Subject: [PATCH] fix: prioritize typed methods over void builtins --- pineforge_codegen/codegen/drawing.py | 43 +++--- tests/test_typed_void_method_precedence.py | 161 +++++++++++++++++++++ 2 files changed, 183 insertions(+), 21 deletions(-) create mode 100644 tests/test_typed_void_method_precedence.py diff --git a/pineforge_codegen/codegen/drawing.py b/pineforge_codegen/codegen/drawing.py index 7091f75..2cc0871 100644 --- a/pineforge_codegen/codegen/drawing.py +++ b/pineforge_codegen/codegen/drawing.py @@ -493,32 +493,34 @@ def _call_is_void(self, node) -> bool: ending in one of these calls must emit it as a statement with a default return, never ``return ;``. """ - if self._drawing_call_is_void(node): - return True if not isinstance(node, FuncCall) or not isinstance(node.callee, MemberAccess): return False method = node.callee.member + direct_spec = self._type_spec_from_expr(node.callee.object) + direct_receiver_name = method_receiver_type_name(direct_spec) + direct_method_info = ( + getattr(self, "_func_info_map", {}).get( + f"{direct_receiver_name}.{method}" + ) + if direct_receiver_name is not None + else None + ) + # Exact typed user methods have dispatch precedence over every builtin + # family. Resolve that once before drawing/array/matrix/map terminal + # classification, including when a parameter named after a namespace + # (for example ``array`` or ``label``) is the authored receiver. + if direct_method_info is not None and getattr( + direct_method_info, "is_udt_method", False + ): + return False + + if self._drawing_call_is_void(node): + return True _fn, ns = self._resolve_callee(node.callee) if method in _MATRIX_VOID_METHODS: # Resolve the exact matrix receiver for both established spellings: - # ``matrix.set(id, ...)`` and ``id.set(...)``. A same-named typed - # user method has dispatch precedence over the builtin (KI-73), so - # never classify that call from the builtin method name alone. - direct_spec = self._type_spec_from_expr(node.callee.object) - direct_receiver_name = method_receiver_type_name(direct_spec) - direct_method_info = ( - getattr(self, "_func_info_map", {}).get( - f"{direct_receiver_name}.{method}" - ) - if direct_receiver_name is not None - else None - ) - if direct_method_info is not None and getattr( - direct_method_info, "is_udt_method", False - ): - return False - + # ``matrix.set(id, ...)`` and ``id.set(...)``. functional_namespace = ns == "matrix" and ( direct_spec is None or direct_spec.kind != "matrix" ) @@ -537,8 +539,7 @@ def _call_is_void(self, node) -> bool: # method form on a std::vector receiver. if ns == "array": return True - recv_spec = self._type_spec_from_expr(node.callee.object) - if recv_spec is not None and recv_spec.kind == "array": + if direct_spec is not None and direct_spec.kind == "array": return True if method not in _MAP_VOID_METHODS: diff --git a/tests/test_typed_void_method_precedence.py b/tests/test_typed_void_method_precedence.py new file mode 100644 index 0000000..573ebd0 --- /dev/null +++ b/tests/test_typed_void_method_precedence.py @@ -0,0 +1,161 @@ +"""Typed user methods must precede builtin void-terminal classification.""" + +from __future__ import annotations + +import pytest + +from pineforge_codegen import transpile +from tests._compile import compile_cpp + + +CASES = ( + ( + "drawing-direct", + '''//@version=6 +strategy("drawing direct precedence") +method set_text(label self) => 11 +method delegate(label self) => self.set_text() +var label drawing_usage = label.new(bar_index, close, "usage") +''', + "_udt_label_delegate", + "return _udt_label_set_text(self);", + ), + ( + "array-direct", + '''//@version=6 +strategy("array direct precedence") +method push(array self) => 12 +method delegate(array self) => self.push() +''', + "_udt_array_int_delegate", + "return _udt_array_int_push(self);", + ), + ( + "map-direct", + '''//@version=6 +strategy("map direct precedence") +method clear(map self) => 13 +method delegate(map self) => self.clear() +''', + "_udt_map_string_int_delegate", + "return _udt_map_string_int_clear(self);", + ), + ( + "drawing-shadow", + '''//@version=6 +strategy("drawing namespace shadow precedence") +method set_text(int self, label other) => 21 +method delegate(int label, label other) => label.set_text(other) +var label drawing_usage = label.new(bar_index, close, "usage") +''', + "_udt_int_delegate", + "return _udt_int_set_text(label, other);", + ), + ( + "array-shadow", + '''//@version=6 +strategy("array namespace shadow precedence") +method push(int self, array other) => 22 +method delegate(int array, array other) => array.push(other) +''', + "_udt_int_delegate", + "return _udt_int_push(array, other);", + ), + ( + "map-shadow", + '''//@version=6 +strategy("map namespace shadow precedence") +method clear(int self, map other) => 23 +method delegate(int map, map other) => map.clear(other) +''', + "_udt_int_delegate", + "return _udt_int_clear(map, other);", + ), +) + + +def _body(cpp: str, function_name: str) -> str: + return cpp.split(f" {function_name}(", 1)[1].split("\n }", 1)[0] + + +@pytest.mark.parametrize( + ("label", "source", "delegate", "expected_return"), + CASES, +) +def test_typed_user_method_precedes_void_builtin_classification( + label: str, + source: str, + delegate: str, + expected_return: str, +) -> None: + cpp = transpile(source) + body = _body(cpp, delegate) + + assert expected_return in body, f"{label}\n{body}" + assert "return 0;" not in body + assert "return 0.0;" not in body + + +def test_all_typed_precedence_cases_native_compile() -> None: + for label, source, _delegate, _expected_return in CASES: + compile_cpp(transpile(source), label=f"typed-void-precedence-{label}") + + +def test_genuine_builtin_void_terminals_keep_default_returns() -> None: + source = '''//@version=6 +strategy("genuine builtin void terminals") +method mutate_label(label self, string text) => self.set_text(text) +method mutate_array(array self) => self.push(1) +method mutate_map(map self) => self.clear() +var label drawing_usage = label.new(bar_index, close, "usage") +''' + + cpp = transpile(source) + for function_name in ( + "_udt_label_mutate_label", + "_udt_array_int_mutate_array", + "_udt_map_string_int_mutate_map", + ): + body = _body(cpp, function_name) + return_lines = [ + line.strip() + for line in body.splitlines() + if line.startswith(" return ") + ] + assert return_lines == ["return 0.0;"] + compile_cpp(cpp, label="typed-void-precedence-genuine-builtins") + + +def test_typed_user_method_precedence_inside_terminal_if() -> None: + source = '''//@version=6 +strategy("typed precedence terminal if") +method push(array self) => 42 +method choose(array self, bool enabled) => + if enabled + self.push() + else + 7 +''' + + cpp = transpile(source) + body = _body(cpp, "_udt_array_int_choose") + + assert "_func_ret = _udt_array_int_push(self);" in body + compile_cpp(cpp, label="typed-void-precedence-terminal-if") + + +def test_typed_user_method_precedence_inside_terminal_switch() -> None: + source = '''//@version=6 +strategy("typed precedence terminal switch") +method clear(int self, map other) => 43 +method choose(int map, map other, int mode) => + switch mode + 1 => map.clear(other) + => 8 +''' + + cpp = transpile(source) + body = _body(cpp, "_udt_int_choose") + + assert "_func_ret = _udt_int_clear(map, other);" in body + compile_cpp(cpp, label="typed-void-precedence-terminal-switch")