diff --git a/pineforge_codegen/analyzer/base.py b/pineforge_codegen/analyzer/base.py index 548d3df..170fa80 100644 --- a/pineforge_codegen/analyzer/base.py +++ b/pineforge_codegen/analyzer/base.py @@ -21,7 +21,13 @@ TupleLiteral, TypeDecl, EnumDecl, MethodDef, TypeField, ) -from ..symbols import PineType, Symbol, SymbolTable, TypeSpec +from ..symbols import ( + PineType, + Symbol, + SymbolTable, + TypeSpec, + method_receiver_type_name, +) from ..errors import SourceLocation, Diagnostic, CompileError, Level, Phase from .. import signatures as sigs from .. import tv_input_choices as tv_in @@ -2183,7 +2189,7 @@ def _resolved_user_call_name(call: FuncCall, owner: str | None) -> str | None: recv = call.callee.object method = call.callee.member - udt_name: str | None = None + receiver_type_name: str | None = None if isinstance(recv, Identifier): owner_info = func_info_by_name.get(owner or "") # Resolve the active callable's lexical parameters before the @@ -2195,24 +2201,31 @@ def _resolved_user_call_name(call: FuncCall, owner: str | None) -> str | None: if (getattr(owner_info, "is_udt_method", False) and owner_info.node.params and recv.name == owner_info.node.params[0]): - udt_name = owner_info.udt_type_name + owner_specs = list( + getattr(owner_info, "param_type_specs", ()) or () + ) + receiver_type_name = method_receiver_type_name( + owner_specs[0] if owner_specs else None + ) or owner_info.udt_type_name elif recv.name in owner_info.node.params: param_idx = owner_info.node.params.index(recv.name) specs = getattr(owner_info, "param_type_specs", []) or [] spec = specs[param_idx] if param_idx < len(specs) else None - if spec is not None and spec.kind == "udt": - udt_name = spec.name + receiver_type_name = method_receiver_type_name(spec) # Resolve the surviving exact global/lexical symbol before the # flat raw-name registry. A later callable-local declaration can # overwrite that registry and otherwise attach the wrong stateful # method edge to wrappers and their written call sites. - if udt_name is None: + if receiver_type_name is None: spec = self._type_spec_from_expr(recv) - if spec is not None and spec.kind == "udt": - udt_name = spec.name - if udt_name is None and isinstance(recv, Identifier): - udt_name = self._udt_var_types.get(recv.name) - key = f"{udt_name}.{method}" if udt_name else "" + receiver_type_name = method_receiver_type_name(spec) + if receiver_type_name is None and isinstance(recv, Identifier): + receiver_type_name = self._udt_var_types.get(recv.name) + key = ( + f"{receiver_type_name}.{method}" + if receiver_type_name + else "" + ) return key if key in func_defs else None def _find_calls(node, known_funcs: set[str], @@ -4361,7 +4374,7 @@ def _visit_EnumDecl(self, node) -> PineType: return PineType.VOID def _visit_MethodDef(self, node) -> PineType: - """Register UDT instance method under a unique key ``TypeName.methodName``.""" + """Register a typed instance method under ``TypeName.methodName``.""" method_key = f"{node.type_name}.{node.name}" self._symbols.enter_scope(f"method_{node.type_name}_{node.name}") loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1) @@ -4369,7 +4382,6 @@ def _visit_MethodDef(self, node) -> PineType: param_types: list[PineType] = [] param_specs: list = [] for i, p in enumerate(node.params): - udt_self = node.type_name if i == 0 else None hint = param_hints[i] if i < len(param_hints) else None # Only the receiver is required to be typed in Pine methods. Every # other omitted type is polymorphic per written call, exactly like @@ -4377,6 +4389,11 @@ def _visit_MethodDef(self, node) -> PineType: # bool/int history call silently coerce through Series. ptype = self._type_hint_to_pine(hint) if hint else PineType.UNKNOWN pspec = self._type_spec_from_hint(hint) if hint else None + udt_self = ( + node.type_name + if i == 0 and pspec is not None and pspec.kind == "udt" + else None + ) param_types.append(ptype) param_specs.append(pspec) sym = Symbol( @@ -4897,12 +4914,9 @@ def _visit_FuncCall(self, node: FuncCall) -> PineType: # apply the same deferred map-history gate as regular UDFs before # codegen can emit the parameter as a scalar double. receiver_spec = self._type_spec_from_expr(obj) - if ( - receiver_spec is not None - and receiver_spec.kind == "udt" - and receiver_spec.name - ): - method_key = f"{receiver_spec.name}.{member}" + receiver_type_name = method_receiver_type_name(receiver_spec) + if receiver_type_name is not None: + method_key = f"{receiver_type_name}.{member}" method_info = next( ( info diff --git a/pineforge_codegen/analyzer/types.py b/pineforge_codegen/analyzer/types.py index f1e400d..49af7c8 100644 --- a/pineforge_codegen/analyzer/types.py +++ b/pineforge_codegen/analyzer/types.py @@ -44,7 +44,7 @@ MemberAccess, NaLiteral, NumberLiteral, StringLiteral, Subscript, Ternary, SwitchStmt, TupleLiteral, UnaryOp, ) -from ..symbols import PineType, TypeSpec +from ..symbols import PineType, TypeSpec, method_receiver_type_name # Drawing-objects-as-data type names (spec §4.1). Defined locally — the # analyzer must not import from ``codegen`` (codegen imports analyzer, so the @@ -241,6 +241,14 @@ def _nullable_collection_selection_spec( def _type_spec_from_expr(self, value: ASTNode | None) -> TypeSpec | None: if value is None: return None + if isinstance(value, NumberLiteral): + return TypeSpec.primitive( + "float" if isinstance(value.value, float) else "int" + ) + if isinstance(value, BoolLiteral): + return TypeSpec.primitive("bool") + if isinstance(value, StringLiteral): + return TypeSpec.primitive("string") if isinstance(value, Ternary): true_spec = self._type_spec_from_expr(value.true_val) false_spec = self._type_spec_from_expr(value.false_val) @@ -375,6 +383,42 @@ def direct_user_udt_ctor_name(node: ASTNode) -> str | None: func = cal.member if isinstance(cal, MemberAccess) else None ns = cal.object.name if isinstance(cal, MemberAccess) and isinstance(cal.object, Identifier) else None targs = self._template_args_from_call(value) + if isinstance(cal, MemberAccess): + typed_receiver_spec = self._type_spec_from_expr(cal.object) + typed_receiver_name = method_receiver_type_name( + typed_receiver_spec + ) + method_info = next( + ( + info + for info in getattr(self, "_func_infos", ()) + if info.name == f"{typed_receiver_name}.{func}" + and getattr(info, "is_udt_method", False) + ), + None, + ) if typed_receiver_name is not None else None + if method_info is not None: + return_spec = getattr( + method_info, "return_type_spec", None + ) + if return_spec is not None: + return return_spec + udt_return = getattr( + method_info, "udt_return_type", None + ) + if udt_return is not None: + return TypeSpec.udt(udt_return) + if method_info.return_type in { + PineType.INT, + PineType.FLOAT, + PineType.BOOL, + PineType.STRING, + PineType.COLOR, + }: + return self._pine_type_to_spec( + method_info.return_type + ) + return None # Drawing-objects-as-data return typing: *.new / *.copy -> handle of # the self-type; linefill.get_line* -> line; chart.point.* -> point. if ns in _DRAWING_NS: @@ -508,10 +552,9 @@ def direct_user_udt_ctor_name(node: ASTNode) -> str | None: return recv_spec.element if func == "eigenvalues": return TypeSpec.array(TypeSpec.primitive("float")) - if (recv_spec is not None - and recv_spec.kind == "udt" - and recv_spec.name): - method_key = f"{recv_spec.name}.{func}" + receiver_name = method_receiver_type_name(recv_spec) + if receiver_name is not None: + method_key = f"{receiver_name}.{func}" method_info = next( ( info @@ -535,15 +578,33 @@ def direct_user_udt_ctor_name(node: ASTNode) -> str | None: return TypeSpec.udt("line") if isinstance(value, Identifier): sym = self._symbols.resolve(value.name) - if sym is not None and sym.type_spec is not None: - return sym.type_spec + if sym is not None: + if sym.type_spec is not None: + return sym.type_spec + if sym.pine_type in { + PineType.INT, + PineType.FLOAT, + PineType.BOOL, + PineType.STRING, + PineType.COLOR, + }: + return self._pine_type_to_spec(sym.pine_type) if isinstance(value, FuncCall): # User-function return spec (e.g. an array-returning # ``buildPDLevels() => array.from(...)``), so a caller's # ``allLevels = buildPDLevels()`` infers an array TypeSpec. cal = value.callee - fname = cal.member if isinstance(cal, MemberAccess) else ( - cal.name if isinstance(cal, Identifier) else None) + if isinstance(cal, MemberAccess): + receiver_name = method_receiver_type_name( + self._type_spec_from_expr(cal.object) + ) + fname = ( + f"{receiver_name}.{cal.member}" + if receiver_name is not None + else cal.member + ) + else: + fname = cal.name if isinstance(cal, Identifier) else None if fname and fname in getattr(self, "_func_return_type_specs", {}): return self._func_return_type_specs[fname] if fname and fname in getattr(self, "_func_udt_return_types", {}): diff --git a/pineforge_codegen/codegen/base.py b/pineforge_codegen/codegen/base.py index 1f886c9..7ea6599 100644 --- a/pineforge_codegen/codegen/base.py +++ b/pineforge_codegen/codegen/base.py @@ -25,7 +25,7 @@ TA_NO_CTOR, TA_PERIOD_ARG, ) -from ..symbols import PineType, TypeSpec +from ..symbols import PineType, TypeSpec, method_receiver_type_name from .. import signatures as sigs from ..errors import CompileError, Diagnostic, Level, Phase, SourceLocation @@ -3391,7 +3391,12 @@ def owner_lexical_specs(owner: str | None) -> dict[str, TypeSpec | None]: and info.node.params and info.udt_type_name ): - lexical[info.node.params[0]] = TypeSpec.udt(info.udt_type_name) + receiver_spec = specs[0] if specs else None + if receiver_spec is None: + receiver_spec = self._type_spec_from_hint_name( + info.udt_type_name + ) + lexical[info.node.params[0]] = receiver_spec lexical.update(self._func_collection_types.get(owner, {})) return lexical @@ -3414,14 +3419,9 @@ def owner_lexical_specs(owner: str | None) -> dict[str, TypeSpec | None]: receiver_spec = self._map_effect_type_spec( node.callee.object, owner_lexical_specs(owner) ) - if ( - receiver_spec is not None - and receiver_spec.kind == "udt" - and receiver_spec.name - ): - method_key = ( - f"{receiver_spec.name}.{node.callee.member}" - ) + receiver_name = method_receiver_type_name(receiver_spec) + if receiver_name is not None: + method_key = f"{receiver_name}.{node.callee.member}" candidate = self._func_info_map.get(method_key) if ( candidate is not None diff --git a/pineforge_codegen/codegen/emit_top.py b/pineforge_codegen/codegen/emit_top.py index f5f89f9..5ea7714 100644 --- a/pineforge_codegen/codegen/emit_top.py +++ b/pineforge_codegen/codegen/emit_top.py @@ -86,7 +86,7 @@ ExprStmt, FuncCall, Identifier, IfStmt, SwitchStmt, VarDecl, ) from ..analyzer import FuncInfo -from ..symbols import PineType +from ..symbols import PineType, method_receiver_cpp_token from .tables import ( BAR_SERIES_PUSH, DRAWING_TYPE_TO_CPP, @@ -1184,10 +1184,18 @@ def _emit_extern_c(self, lines: list[str]) -> None: lines.append("") def _emit_udt_method_cpp_name(self, fi: FuncInfo) -> str: - """Stable C++ identifier for a UDT instance method (``_udt_Type_method``).""" - udt = fi.udt_type_name or "" + """Stable C++ identifier for a typed instance method.""" + receiver_spec = ( + fi.param_type_specs[0] + if getattr(fi, "param_type_specs", None) + else None + ) + receiver = method_receiver_cpp_token( + receiver_spec, + fi.udt_type_name, + ) base = fi.node.name if fi.node else "" - return self._func_safe_name(f"_udt_{udt}_{base}") + return self._func_safe_name(f"_udt_{receiver}_{base}") def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | None = None, instance: dict | None = None) -> None: @@ -1228,7 +1236,10 @@ def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | No self._map_vars = set(prev_map_vars) self._matrix_specs = dict(prev_matrix_specs) - is_udt = bool(getattr(fi, "is_udt_method", False)) and fi.udt_type_name + is_method = ( + bool(getattr(fi, "is_udt_method", False)) + and fi.udt_type_name + ) # Determine param types and set context for type inference inside body param_strs = [] @@ -1261,22 +1272,54 @@ def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | No ) for i, p in enumerate(node.params): spec = None - if is_udt and i == 0 and fi.udt_type_name: - # A method receiver whose type is a drawing primitive - # (egoigor's ``method slope(line ln)``) must emit ``Line&`` not - # the unknown ``line&``. Register _udt_param_udt so the body's - # getters dispatch through the §4.3 drawing path (L.6d / U.5). + receiver_spec = None + if is_method and i == 0: recv_spec = ( fi.param_type_specs[i] if i < len(fi.param_type_specs) else None ) - if recv_spec is not None and recv_spec.kind == "map": - # A map method receiver is a copied ID handle. Mutations - # reach the caller's map while rebinds stay method-local. - spec = recv_spec + receiver_spec = recv_spec + + if ( + receiver_spec is not None + and receiver_spec.kind == "primitive" + and p in func_sv + ): + # A primitive receiver used with history is a Series boundary, + # just like an ordinary history-bearing UDF parameter. + elem_cpp_t = self._series_param_element_cpp_type( + fi, i, call_site_idx + ) + cpp_t = f"const Series<{elem_cpp_t}>&" + spec = receiver_spec + self._current_func_series_params.add(p) + self._current_func_series_param_types[p] = elem_cpp_t + self._current_func_series_param_types[ + self._safe_name(p) + ] = elem_cpp_t + elif is_method and i == 0 and fi.udt_type_name: + # Receiver pass modes follow Pine's value/ID families: + # primitives by value; arrays, matrices, UDTs, and drawings by + # reference; maps by copied shared-ID handle so mutations + # reach the caller while receiver rebinds remain local. + recv_spec = receiver_spec + if recv_spec is None: + recv_spec = self._type_spec_from_hint_name( + fi.udt_type_name + ) + spec = recv_spec + if recv_spec is not None: cpp_t = self._type_spec_to_cpp(recv_spec) + if recv_spec.kind in {"array", "matrix", "udt"}: + cpp_t = f"{cpp_t}&" + if recv_spec.kind == "udt" and recv_spec.name: + safe_p = self._safe_name(p) + self._udt_param_udt[safe_p] = recv_spec.name + self._udt_param_udt[p] = recv_spec.name else: + # Compatibility for synthetic/legacy method records that + # carry only the old receiver-name field. recv_cpp = DRAWING_TYPE_TO_CPP.get( fi.udt_type_name, fi.udt_type_name ) @@ -1401,7 +1444,11 @@ def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | No ) # For per-call-site variants, suffix the function name and activate TA + var remapping - func_name = self._emit_udt_method_cpp_name(fi) if is_udt else self._func_safe_name(fi.name) + func_name = ( + self._emit_udt_method_cpp_name(fi) + if is_method + else self._func_safe_name(fi.name) + ) if instance is not None: # Fresh context-sensitive instance: name + composed remaps come from # the instance record. No textual cs index (dispatch is via the diff --git a/pineforge_codegen/codegen/types.py b/pineforge_codegen/codegen/types.py index 44e4dfc..1cd60f6 100644 --- a/pineforge_codegen/codegen/types.py +++ b/pineforge_codegen/codegen/types.py @@ -38,7 +38,7 @@ MemberAccess, NaLiteral, NumberLiteral, StringLiteral, SwitchStmt, Subscript, Ternary, TupleLiteral, UnaryOp, VarDecl, ) -from ..symbols import PineType, TypeSpec +from ..symbols import PineType, TypeSpec, method_receiver_type_name from .. import signatures as sigs from .tables import ( ARRAY_DRAWING_NEW_CTORS, @@ -459,8 +459,18 @@ def _type_spec_from_expr(self, node) -> TypeSpec | None: if self._collection_name_is_lexically_shadowed(node.name): return None sym = self.ctx.symbols.resolve(node.name) - if sym is not None and getattr(sym, "type_spec", None) is not None: - return sym.type_spec + if sym is not None: + if getattr(sym, "type_spec", None) is not None: + return sym.type_spec + primitive_name = { + PineType.INT: "int", + PineType.FLOAT: "float", + PineType.BOOL: "bool", + PineType.STRING: "string", + PineType.COLOR: "color", + }.get(sym.pine_type) + if primitive_name is not None: + return TypeSpec.primitive(primitive_name) return None if isinstance(node, Subscript): # History access preserves the scalar drawing-handle type. This @@ -563,6 +573,57 @@ def _type_spec_from_expr(self, node) -> TypeSpec | None: if (udt_return in DRAWING_TYPE_TO_CPP or udt_return in self._udt_defs): return TypeSpec.udt(udt_return) + if isinstance(node.callee, MemberAccess): + receiver_spec = self._type_spec_from_expr( + node.callee.object + ) + receiver_name = method_receiver_type_name(receiver_spec) + method_info = ( + getattr(self, "_func_info_map", {}).get( + f"{receiver_name}.{node.callee.member}" + ) + if receiver_name is not None + else None + ) + if ( + method_info is not None + and getattr(method_info, "is_udt_method", False) + ): + return_spec = getattr( + method_info, "return_type_spec", None + ) + if return_spec is not None: + return return_spec + udt_return = getattr( + method_info, "udt_return_type", None + ) + if ( + udt_return in DRAWING_TYPE_TO_CPP + or udt_return in self._udt_defs + ): + return TypeSpec.udt(udt_return) + call_site_idx = self._callable_target_callsite_idx( + method_info, + node, + ) + return_pine_type = ( + self._callsite_callable_return_pine_type( + method_info, + call_site_idx, + ) + ) + primitive_name = { + PineType.INT: "int", + PineType.FLOAT: "float", + PineType.BOOL: "bool", + PineType.STRING: "string", + PineType.COLOR: "color", + }.get(return_pine_type) + return ( + TypeSpec.primitive(primitive_name) + if primitive_name is not None + else None + ) # 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 @@ -679,11 +740,12 @@ def _type_spec_from_expr(self, node) -> TypeSpec | None: return TypeSpec.primitive("bool") if member_name == "size": return TypeSpec.primitive("int") - if (recv_spec is not None - and recv_spec.kind == "udt" - and recv_spec.name): - method_info = self._func_info_map.get( - f"{recv_spec.name}.{member_name}" + receiver_name = method_receiver_type_name(recv_spec) + if receiver_name is not None: + method_info = getattr( + self, "_func_info_map", {} + ).get( + f"{receiver_name}.{member_name}" ) return_spec = getattr( method_info, "return_type_spec", None @@ -1255,20 +1317,25 @@ def _expr_returns_wide_int( and owner_node.params and isinstance(receiver, Identifier) and receiver.name == owner_node.params[0] - and getattr(owner_info, "udt_type_name", None) ): - callee_info = self._func_info_map.get( - f"{owner_info.udt_type_name}.{expr.callee.member}" + receiver_specs = list( + getattr(owner_info, "param_type_specs", ()) or () ) + receiver_name = method_receiver_type_name( + receiver_specs[0] if receiver_specs else None + ) or getattr(owner_info, "udt_type_name", None) + if receiver_name is not None: + callee_info = self._func_info_map.get( + f"{receiver_name}.{expr.callee.member}" + ) if callee_info is None: receiver_spec = self._type_spec_from_expr(receiver) - if ( - receiver_spec is not None - and receiver_spec.kind == "udt" - and receiver_spec.name - ): + receiver_name = method_receiver_type_name( + receiver_spec + ) + if receiver_name is not None: callee_info = self._func_info_map.get( - f"{receiver_spec.name}.{expr.callee.member}" + f"{receiver_name}.{expr.callee.member}" ) return ( callee_info is not None @@ -1899,8 +1966,11 @@ def _infer_type(self, node) -> str: recv_spec = self._type_spec_from_expr(node.callee.object) if recv_spec is not None and recv_spec.kind == "array" and member_name == "join": return "std::string" - if recv_spec is not None and recv_spec.kind == "udt" and recv_spec.name: - fi_u = self._func_info_map.get(f"{recv_spec.name}.{member_name}") + receiver_name = method_receiver_type_name(recv_spec) + if receiver_name is not None: + fi_u = self._func_info_map.get( + f"{receiver_name}.{member_name}" + ) if fi_u is not None: call_site_idx = self._callable_target_callsite_idx( fi_u, node diff --git a/pineforge_codegen/codegen/visit_call.py b/pineforge_codegen/codegen/visit_call.py index 2969285..217ea17 100644 --- a/pineforge_codegen/codegen/visit_call.py +++ b/pineforge_codegen/codegen/visit_call.py @@ -142,7 +142,7 @@ StringLiteral, VarDecl, ) -from ..symbols import TypeSpec +from ..symbols import TypeSpec, method_receiver_type_name from .. import signatures as sigs from .drawing import ALL_DRAWING_METHODS from .tables import ( @@ -247,6 +247,86 @@ def _udt_method_call_emit_name(self, fi, node: FuncCall) -> str: return f"{base}_cs{self._active_call_site_idx}" return base + def _typed_user_method_info(self, receiver, member: str): + """Resolve an exact typed user method before any builtin method.""" + receiver_spec = self._type_spec_from_expr(receiver) + receiver_name = method_receiver_type_name(receiver_spec) + if receiver_name is None: + return receiver_spec, None + method_info = self._func_info_map.get(f"{receiver_name}.{member}") + if ( + method_info is None + or not getattr(method_info, "is_udt_method", False) + ): + return receiver_spec, None + return receiver_spec, method_info + + def _emit_typed_user_method_call( + self, + node: FuncCall, + receiver_spec: TypeSpec, + method_info, + ) -> str: + """Lower a typed extension method with ordinary callable semantics.""" + callee = node.callee + assert isinstance(callee, MemberAccess) + receiver_node = callee.object + fn_cpp = self._udt_method_call_emit_name(method_info, node) + + param_names = ( + list(method_info.node.params[1:]) + if method_info.node is not None + else [] + ) + param_defaults = list( + getattr(method_info, "param_defaults", []) or [] + )[1:] + rest_nodes = _merge_kwargs_with_defaults( + node.args, + node.kwargs, + param_names, + param_defaults, + lambda value: value, + ) + receiver_cpp = self._visit_typed_method_param( + method_info, + node, + receiver_node, + 0, + ) + rest_cpp = [ + self._visit_typed_method_param( + method_info, + node, + arg, + index + 1, + ) + for index, arg in enumerate(rest_nodes) + ] + + receiver_root = receiver_node + while isinstance(receiver_root, MemberAccess): + receiver_root = receiver_root.object + receiver_passes_by_reference = receiver_spec.kind in { + "array", + "matrix", + "udt", + } + return self._ordered_user_call_expr( + fn_cpp, + [receiver_node, *rest_nodes], + [receiver_cpp, *rest_cpp], + source_order_nodes=[ + receiver_node, + *node.args, + *node.kwargs.values(), + ], + force_stage=( + receiver_passes_by_reference + and not isinstance(receiver_root, Identifier) + ), + ) + def _callable_target_callsite_idx(self, fi, node: FuncCall) -> int | None: """Return the primitive profile selected by this emitted call path.""" cs_info = self.ctx.func_call_cs_map.get(id(node)) @@ -680,10 +760,9 @@ def _map_effect_callable_info( receiver_spec = self._map_effect_type_spec( callee.object, lexical_specs ) - if (receiver_spec is not None - and receiver_spec.kind == "udt" - and receiver_spec.name): - key = f"{receiver_spec.name}.{callee.member}" + receiver_name = method_receiver_type_name(receiver_spec) + if receiver_name is not None: + key = f"{receiver_name}.{callee.member}" return key, self._func_info_map.get(key) return "", None @@ -790,15 +869,14 @@ def _ordered_user_call_expr( ) return lowered - def _visit_udt_method_series_arg( + def _visit_typed_method_param( self, func_info, call_node: FuncCall, arg_node, - rest_index: int, + param_index: int, ) -> str: - """Lower one UDT-method argument with history-Series awareness.""" - param_index = rest_index + 1 # receiver is parameter 0 + """Lower one typed-method parameter with history-Series awareness.""" param_name = ( func_info.node.params[param_index] if func_info.node is not None @@ -869,9 +947,37 @@ def _visit_udt_method_series_arg( f"return {member}; }}())" ) + def _visit_udt_method_series_arg( + self, + func_info, + call_node: FuncCall, + arg_node, + rest_index: int, + ) -> str: + """Backward-compatible wrapper for non-receiver method arguments.""" + return self._visit_typed_method_param( + func_info, + call_node, + arg_node, + rest_index + 1, + ) + def _visit_func_call(self, node: FuncCall) -> str: callee = node.callee if isinstance(callee, MemberAccess): + recv_spec, method_info = self._typed_user_method_info( + callee.object, + callee.member, + ) + if method_info is not None and recv_spec is not None: + # Pine extension methods are exact type-directed overloads. + # They must win before collection/drawing builtins; otherwise + # a user ``array.push`` silently executes push_back. + return self._emit_typed_user_method_call( + node, + recv_spec, + method_info, + ) recv_spec = self._type_spec_from_expr(callee.object) if ( recv_spec is not None diff --git a/pineforge_codegen/codegen/visit_stmt.py b/pineforge_codegen/codegen/visit_stmt.py index 4ca77b3..e6d75f7 100644 --- a/pineforge_codegen/codegen/visit_stmt.py +++ b/pineforge_codegen/codegen/visit_stmt.py @@ -98,7 +98,7 @@ VarDecl, WhileStmt, ) -from ..symbols import PineType, TypeSpec +from ..symbols import PineType, TypeSpec, method_receiver_type_name from .tables import ( ARRAY_NEW_CTORS, DRAWING_TYPE_TO_CPP, @@ -1329,8 +1329,9 @@ def emit_call_tuple(call_expr: str) -> None: callee = node.value.callee if isinstance(callee, MemberAccess): recv_spec = self._type_spec_from_expr(callee.object) - if recv_spec is not None and recv_spec.kind == "udt" and recv_spec.name: - method_key = f"{recv_spec.name}.{callee.member}" + receiver_name = method_receiver_type_name(recv_spec) + if receiver_name is not None: + method_key = f"{receiver_name}.{callee.member}" fi_u = self._func_info_map.get(method_key) if (fi_u is not None and getattr(fi_u, "is_udt_method", False) @@ -1352,11 +1353,10 @@ def _tuple_binding_cpp_types(self, node: TupleAssign) -> list[str]: fi = self._func_info_map.get(func_name) elif isinstance(node.value.callee, MemberAccess): recv_spec = self._type_spec_from_expr(node.value.callee.object) - if (recv_spec is not None - and recv_spec.kind == "udt" - and recv_spec.name): + receiver_name = method_receiver_type_name(recv_spec) + if receiver_name is not None: fi = self._func_info_map.get( - f"{recv_spec.name}.{node.value.callee.member}" + f"{receiver_name}.{node.value.callee.member}" ) if (fi is not None and fi.node is not None diff --git a/pineforge_codegen/parser.py b/pineforge_codegen/parser.py index 9ca1e7a..327d3b6 100644 --- a/pineforge_codegen/parser.py +++ b/pineforge_codegen/parser.py @@ -791,8 +791,16 @@ def _parse_method_def(self): start_tok = self._advance() # consume 'method' name = self._consume(TokenType.IDENT).value self._consume(TokenType.LPAREN) - # First param is the type + self: TypeName self - type_name = self._consume(TokenType.IDENT).value + # First param is the required receiver type + self name. Receiver + # types use the same complete Pine hint grammar as ordinary typed + # parameters: primitives lex as TYPE_* tokens, while collections may + # carry nested ``<...>`` arguments or postfix ``[]``. Consuming only + # IDENT here silently dropped valid declarations such as + # ``method id(int self)`` and ``method push(array self, ...)`` + # through the parser's top-level recovery path. + if self._current().type not in TYPE_KEYWORDS | {TokenType.IDENT}: + self._consume(TokenType.IDENT) # raises a located ParseError + type_name = self._parse_type_hint_string() params = [self._consume(TokenType.IDENT).value] # 'self' or user's name param_type_hints = [type_name] # Preserve per-param default expressions so codegen can substitute diff --git a/pineforge_codegen/symbols.py b/pineforge_codegen/symbols.py index 46a3e71..a6b0cba 100644 --- a/pineforge_codegen/symbols.py +++ b/pineforge_codegen/symbols.py @@ -56,9 +56,74 @@ def __str__(self) -> str: return f"array<{self.element}>" if self.kind == "map" and self.key is not None and self.value is not None: return f"map<{self.key},{self.value}>" + if self.kind == "matrix" and self.element is not None: + return f"matrix<{self.element}>" return self.name or self.kind +def method_receiver_type_name(spec: TypeSpec | None) -> str | None: + """Canonical Pine type name used to key a user method receiver. + + ``MethodDef.type_name`` is the parser-normalized spelling (for example + ``array``). Calls are resolved from structured ``TypeSpec`` values, + so both sides need one lossless spelling across primitive, UDT, and + collection receivers. Incomplete collection specs stay unresolved rather + than accidentally colliding under a coarse ``array``/``map``/``matrix`` + key. + """ + + if spec is None: + return None + if spec.kind in {"primitive", "udt"}: + return spec.name + if spec.kind == "array" and spec.element is not None: + element = method_receiver_type_name(spec.element) + return f"array<{element}>" if element is not None else None + if spec.kind == "map" and spec.key is not None and spec.value is not None: + key = method_receiver_type_name(spec.key) + value = method_receiver_type_name(spec.value) + if key is not None and value is not None: + return f"map<{key},{value}>" + return None + if spec.kind == "matrix" and spec.element is not None: + element = method_receiver_type_name(spec.element) + return f"matrix<{element}>" if element is not None else None + return None + + +def method_receiver_cpp_token( + spec: TypeSpec | None, + fallback_name: str | None = None, +) -> str: + """Return a stable C++ identifier fragment for a method receiver type. + + Existing primitive, UDT, and drawing receiver spellings remain unchanged. + Generic Pine types cannot be embedded directly in an identifier, so their + structure is encoded recursively (``array`` -> ``array_int``). The + fallback only supports older/synthetic ``FuncInfo`` records that lack the + structured receiver spec. + """ + + if spec is not None: + if spec.kind in {"primitive", "udt"} and spec.name: + return spec.name + if spec.kind == "array" and spec.element is not None: + return f"array_{method_receiver_cpp_token(spec.element)}" + if spec.kind == "map" and spec.key is not None and spec.value is not None: + return ( + f"map_{method_receiver_cpp_token(spec.key)}_" + f"{method_receiver_cpp_token(spec.value)}" + ) + if spec.kind == "matrix" and spec.element is not None: + return f"matrix_{method_receiver_cpp_token(spec.element)}" + + raw = fallback_name or "receiver" + token = "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in raw) + if token and token[0].isdigit(): + token = f"type_{token}" + return token or "receiver" + + @dataclass class Symbol: name: str diff --git a/tests/test_codegen_ta_stable_var_literal.py b/tests/test_codegen_ta_stable_var_literal.py index 1de6eaf..0e25d20 100644 --- a/tests/test_codegen_ta_stable_var_literal.py +++ b/tests/test_codegen_ta_stable_var_literal.py @@ -15,6 +15,7 @@ import pytest from pineforge_codegen import transpile +from pineforge_codegen.ast_nodes import MethodDef from pineforge_codegen.errors import CompileError, Level, Phase from pineforge_codegen.lexer import Lexer from pineforge_codegen.parser import Parser @@ -75,6 +76,29 @@ def _assert_parse_recovery_fences_sma(source: str, *, expected_line: int) -> Non ) +def _assert_method_declaration_fences_sma( + source: str, + *, + expected_line: int, +) -> None: + program = Parser(Lexer(source).tokenize(), source=source).parse() + assert (program.annotations or {}).get("parse_recovery_count", 0) == 0 + assert any(isinstance(node, MethodDef) for node in program.body) + + with pytest.raises(CompileError) as caught: + transpile(source, filename="stable-var-method-declaration.pine") + + assert len(caught.value.diagnostics) == 1 + diagnostic = caught.value.diagnostics[0] + assert diagnostic.phase is Phase.CODEGEN + assert diagnostic.location.line == expected_line + assert diagnostic.message == ( + "Unsupported TA constructor length 'p' for ta::SMA: it is neither a " + "compile-time constant nor derived from an input, so PineForge cannot " + "size the indicator buffer." + ) + + @pytest.mark.parametrize( "declaration", [ @@ -229,7 +253,7 @@ def test_any_user_declaration_fences_narrow_literal_admission( ) -def test_dropped_primitive_method_declaration_fences_unrelated_receiver_mask() -> None: +def test_primitive_method_declaration_fences_narrow_literal_admission() -> None: source = '''//@version=6 strategy("parse recovery receiver mask") method id(int self) => self @@ -239,10 +263,10 @@ def test_dropped_primitive_method_declaration_fences_unrelated_receiver_mask() - z = ta.sma(close, p) ''' - _assert_parse_recovery_fences_sma(source, expected_line=7) + _assert_method_declaration_fences_sma(source, expected_line=7) -def test_dropped_collection_method_cannot_collide_with_builtin_semantics() -> None: +def test_collection_method_declaration_fences_narrow_literal_admission() -> None: source = '''//@version=6 strategy("parse recovery builtin collision") method push(array self, int x) => array.unshift(self, x) @@ -252,7 +276,7 @@ def test_dropped_collection_method_cannot_collide_with_builtin_semantics() -> No z = ta.sma(close, p) ''' - _assert_parse_recovery_fences_sma(source, expected_line=7) + _assert_method_declaration_fences_sma(source, expected_line=7) @pytest.mark.parametrize( diff --git a/tests/test_method_receiver_types.py b/tests/test_method_receiver_types.py new file mode 100644 index 0000000..99f4e5b --- /dev/null +++ b/tests/test_method_receiver_types.py @@ -0,0 +1,141 @@ +"""Lossless parsing and dispatch for typed Pine v6 method receivers. + +Primitive and generic receiver hints must survive parsing. In particular, a +user method whose name collides with a collection builtin must win dispatch; +otherwise the authored declaration can disappear while valid but wrong C++ is +emitted for the builtin body. +""" + +from __future__ import annotations + +import re + +import pytest + +from pineforge_codegen import transpile +from pineforge_codegen.ast_nodes import MethodDef +from pineforge_codegen.lexer import Lexer +from pineforge_codegen.parser import Parser +from tests._compile import compile_cpp + + +@pytest.mark.parametrize( + ("receiver_hint", "expected_hint"), + [ + ("int", "int"), + ("float", "float"), + ("bool", "bool"), + ("string", "string"), + ("array", "array"), + ("map", "map"), + ("matrix", "matrix"), + ("int[]", "array"), + ], +) +def test_method_receiver_type_hint_parses_without_recovery( + receiver_hint: str, + expected_hint: str, +) -> None: + source = f'''//@version=6 +strategy("typed method receiver") +method inspect({receiver_hint} self) => 1 +result = 0 +''' + + program = Parser(Lexer(source).tokenize(), source=source).parse() + assert (program.annotations or {}).get("parse_recovery_count", 0) == 0 + methods = [node for node in program.body if isinstance(node, MethodDef)] + assert len(methods) == 1 + assert methods[0].type_name == expected_hint + assert methods[0].params == ["self"] + assert methods[0].annotations["param_type_hints"] == [expected_hint] + + +def test_primitive_receiver_method_emits_authored_body() -> None: + source = '''//@version=6 +strategy("primitive receiver method") +method add(int self, int delta) => self + delta + 100 +result = 1.add(4) +''' + + cpp = transpile(source) + assert re.search(r"int _udt_int_add\(int self, int delta\)", cpp) + assert "_udt_int_add(1, 4)" in cpp + assert "None()" not in cpp + compile_cpp(cpp, label="primitive-method-receiver") + + +def test_primitive_history_receiver_uses_series_boundary() -> None: + source = '''//@version=6 +strategy("primitive history receiver") +method previous(int self) => self[1] +current = bar_index +result = current.previous() +''' + + cpp = transpile(source) + assert re.search( + r"int64_t _udt_int_previous(?:_cs0)?\(const Series& self\)", + cpp, + ) + assert "current.previous()" not in cpp + assert re.search(r"_udt_int_previous(?:_cs0)?\(\(\[&\]\(\)", cpp) + compile_cpp(cpp, label="primitive-history-method-receiver") + + +def test_array_user_method_precedes_same_named_builtin() -> None: + source = '''//@version=6 +strategy("array builtin collision") +method push(array self, int value) => array.unshift(self, value + 100) +var array values = array.new() +values.push(7) +result = values.get(0) +''' + + cpp = transpile(source) + assert re.search(r"_udt_array_int_+push\(values, 7\)", cpp) + assert "values.push_back(7);" not in cpp + assert "self.insert(self.begin(), (value + 100));" in cpp + compile_cpp(cpp, label="array-method-builtin-collision") + + +def test_map_receiver_keeps_shared_id_value_handle() -> None: + source = '''//@version=6 +strategy("map builtin collision") +method put(map self, string key, int value) => map.put(self, key, value + 100) +var map values = map.new() +values.put("answer", 7) +result = values.get("answer") +''' + + cpp = transpile(source) + assert re.search( + r"_udt_map_string_int_put\(PineMap self, " + r"std::string key, int value\)", + cpp, + ) + assert '_udt_map_string_int_put(values, std::string("answer"), 7)' in cpp + assert 'values.put(std::string("answer"), 7);' not in cpp + compile_cpp(cpp, label="map-method-builtin-collision") + + +def test_matrix_receiver_emits_by_reference_before_builtin() -> None: + source = '''//@version=6 +strategy("matrix builtin collision") +method set(matrix self, int row, int col, int value) => + matrix.set(self, row, col, value + 100) + matrix.get(self, row, col) +var matrix values = matrix.new(1, 1, 0) +values.set(0, 0, 7) +result = values.get(0, 0) +''' + + cpp = transpile(source) + assert re.search( + r"_udt_matrix_int_set\(PineGenericMatrix& self, int row, " + r"int col, int value\)", + cpp, + ) + assert "_udt_matrix_int_set(values, 0, 0, 7)" in cpp + assert "values.set(0, 0, 7);" not in cpp + compile_cpp(cpp, label="matrix-method-builtin-collision")