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
52 changes: 33 additions & 19 deletions pineforge_codegen/analyzer/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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],
Expand Down Expand Up @@ -4361,22 +4374,26 @@ 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)
param_hints = (node.annotations or {}).get("param_type_hints", [])
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
# a regular UDF parameter; seeding it as FLOAT makes even a single
# bool/int history call silently coerce through Series<double>.
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(
Expand Down Expand Up @@ -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
Expand Down
79 changes: 70 additions & 9 deletions pineforge_codegen/analyzer/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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", {}):
Expand Down
20 changes: 10 additions & 10 deletions pineforge_codegen/codegen/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
77 changes: 62 additions & 15 deletions pineforge_codegen/codegen/emit_top.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading