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
27 changes: 20 additions & 7 deletions pineforge_codegen/analyzer/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1497,6 +1497,20 @@ def _visit_FuncDef(self, node: FuncDef) -> PineType:
inferred_param_specs[i] = spec
self._func_param_type_specs[node.name] = inferred_param_specs

# Capture direct terminal map-call metadata before leaving the lexical
# function scope. Local map variables and typed map parameters are no
# longer resolvable from the symbol table after ``exit_scope()``. Keep
# this narrow to map terminals so general/nonterminal inference and
# generated output remain unchanged.
terminal_ret_expr = self._direct_terminal_return_expr(node)
terminal_map_return = self._terminal_map_call_return(
terminal_ret_expr,
{
name: spec
for name, spec in zip(node.params, inferred_param_specs)
},
)

self._symbols.exit_scope()

# Detect if function returns a tuple (last stmt is TupleLiteral)
Expand All @@ -1522,13 +1536,7 @@ def _visit_FuncDef(self, node: FuncDef) -> PineType:
# to propagate UDT typing onto the caller's local. Probe:
# data/validation/udt-method-probe-20-udt-return-from-func.
if node.body:
last_stmt = node.body[-1]
ret_expr = None
if isinstance(last_stmt, ExprStmt):
ret_expr = last_stmt.expr
elif not isinstance(last_stmt, (TupleLiteral,)):
# last_stmt is itself an expression node (single-expr funcs)
ret_expr = last_stmt if hasattr(last_stmt, "loc") else None
ret_expr = terminal_ret_expr
udt_ret = self._udt_name_from_ctor(ret_expr) if ret_expr is not None else None
if udt_ret is None:
# Drawing-handle returns wrapped in an if-statement terminal
Expand All @@ -1548,6 +1556,11 @@ def _visit_FuncDef(self, node: FuncDef) -> PineType:
if ret_spec is not None and ret_spec.kind == "array":
self._func_return_type_specs[node.name] = ret_spec

if terminal_map_return is not None:
body_type, terminal_spec = terminal_map_return
if terminal_spec is not None:
self._func_return_type_specs[node.name] = terminal_spec

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

Expand Down
32 changes: 30 additions & 2 deletions pineforge_codegen/analyzer/call_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,6 @@ def _handle_user_func_call(self, func_name: str, node: FuncCall) -> PineType:
# Forward UDT-return inference (set in _visit_FuncDef) so codegen can
# emit the struct return type. Probe: udt-method-probe-20.
udt_ret = self._func_udt_return_types.get(func_name)
ret_spec = getattr(self, "_func_return_type_specs", {}).get(func_name)
# Per-param TypeSpec: declared hints are authoritative; for untyped
# params, infer from the call-site argument's type_spec (so an untyped
# ``s`` used as a string, or a UDT passed by value, emits correctly).
Expand All @@ -1178,6 +1177,32 @@ def _handle_user_func_call(self, func_name: str, node: FuncCall) -> PineType:
if param_specs[i] is None and i < len(arg_specs):
param_specs[i] = arg_specs[i]
existing = [fi for fi in self._func_infos if fi.name == func_name]

# A direct terminal map call on an untyped parameter cannot be typed
# while the function definition is first visited: its map TypeSpec is
# learned only here, from the call-site argument. Re-run only the
# terminal-map classifier with those established parameter specs; do
# not re-analyze the body or participate in general return inference.
effective_param_specs = list(param_specs)
if existing and existing[0].param_type_specs:
for i, spec in enumerate(existing[0].param_type_specs):
if i < len(effective_param_specs) and spec is not None:
effective_param_specs[i] = spec
terminal_map_return = self._terminal_map_call_return(
self._direct_terminal_return_expr(func_def),
{
name: spec
for name, spec in zip(func_def.params, effective_param_specs)
},
)
ret_spec = getattr(self, "_func_return_type_specs", {}).get(func_name)
if terminal_map_return is not None:
return_type, inferred_ret_spec = terminal_map_return
self._func_return_types[func_name] = return_type
if inferred_ret_spec is not None:
self._func_return_type_specs[func_name] = inferred_ret_spec
ret_spec = inferred_ret_spec

if not existing:
fi = FuncInfo(
name=func_name,
Expand All @@ -1194,7 +1219,10 @@ def _handle_user_func_call(self, func_name: str, node: FuncCall) -> PineType:
else:
# Update with better type info if available
fi = existing[0]
if fi.return_type in (PineType.UNKNOWN, PineType.VOID) and return_type not in (PineType.UNKNOWN, PineType.VOID):
if terminal_map_return is not None:
fi.return_type = return_type
fi.return_type_spec = ret_spec
elif fi.return_type in (PineType.UNKNOWN, PineType.VOID) and return_type not in (PineType.UNKNOWN, PineType.VOID):
fi.return_type = return_type
for i, pt in enumerate(param_types):
if i < len(fi.param_types) and fi.param_types[i] == PineType.UNKNOWN:
Expand Down
8 changes: 4 additions & 4 deletions pineforge_codegen/analyzer/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ class FuncInfo:
# 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 — today this covers
# array-returning functions (``buildPDLevels() => array.from(...)`` ->
# ``std::vector<double>``). UDT / drawing-handle returns use
# 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``.
return_type_spec: Any = None

Expand Down Expand Up @@ -231,7 +231,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 array-return TypeSpec (see FuncInfo.return_type_spec).
# Per-function collection-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
143 changes: 141 additions & 2 deletions pineforge_codegen/analyzer/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
from typing import Any

from ..ast_nodes import (
ASTNode, BoolLiteral, FuncCall, Identifier, MemberAccess,
NaLiteral, NumberLiteral, StringLiteral, UnaryOp,
ASTNode, BoolLiteral, ExprStmt, FuncCall, Identifier, MemberAccess,
NaLiteral, NumberLiteral, StringLiteral, TupleLiteral, UnaryOp,
)
from ..symbols import PineType, TypeSpec

Expand Down Expand Up @@ -257,6 +257,145 @@ def _type_spec_from_expr(self, value: ASTNode | None) -> TypeSpec | None:
return (self._udt_field_type_specs.get(owner.name) or {}).get(value.member)
return None

@staticmethod
def _direct_terminal_return_expr(func_def) -> ASTNode | None:
"""Return a UDF's direct terminal expression, if it has one."""
if not func_def.body:
return None
last_stmt = func_def.body[-1]
if isinstance(last_stmt, ExprStmt):
return last_stmt.expr
if not isinstance(last_stmt, TupleLiteral) and hasattr(last_stmt, "loc"):
return last_stmt
return None

def _terminal_map_call_return(
self,
value: ASTNode | None,
parameter_specs: dict[str, TypeSpec | None] | None = None,
) -> tuple[PineType, TypeSpec | None] | None:
"""Return metadata for a direct terminal map call in a UDF.

This deliberately does not participate in general expression or
declaration inference. During function-definition analysis it uses
the active lexical scope for locals and typed parameters; for untyped
parameters it may be called again with call-site ``parameter_specs``.
Arbitrary-expression receivers stay out of scope because their map
method routing is not supported yet.
"""
if not isinstance(value, FuncCall) or not isinstance(
value.callee, MemberAccess
):
return None

callee = value.callee
method = callee.member
receiver = None
if isinstance(callee.object, Identifier) and callee.object.name == "map":
# Functional form: map.method(id, ...). Keyword-only receiver
# routing remains a separate residual, so require the established
# positional receiver here.
functional_arity = {
"clear": 1,
"keys": 1,
"values": 1,
"copy": 1,
"put_all": 2,
"get": 2,
"remove": 2,
"put": 3,
}.get(method)
if (
functional_arity is not None
and len(value.args) == functional_arity
and not value.kwargs
):
receiver = value.args[0]
elif isinstance(callee.object, Identifier):
# Global/local/typed-parameter method forms only. Do not infer an
# arbitrary expression receiver that codegen cannot route.
is_parameter = (
parameter_specs is not None
and callee.object.name in parameter_specs
)
valid_method_shape = False
expected_arity = {
"clear": 0,
"keys": 0,
"values": 0,
"copy": 0,
"put_all": 1,
"get": 1,
"remove": 1,
"put": 2,
}.get(method)
if expected_arity is not None:
valid_method_shape = (
len(value.args) == expected_arity and not value.kwargs
)
if is_parameter and not valid_method_shape:
if method == "put_all":
valid_method_shape = (
not value.args and set(value.kwargs) == {"id2"}
)
elif method in ("get", "remove"):
valid_method_shape = (
not value.args and set(value.kwargs) == {"key"}
)
elif method == "put":
valid_method_shape = (
(
len(value.args) == 1
and set(value.kwargs) == {"value"}
)
or (
not value.args
and set(value.kwargs) == {"key", "value"}
)
)
if valid_method_shape:
receiver = callee.object
if receiver is None:
return None

recv_spec = None
if (
isinstance(receiver, Identifier)
and parameter_specs is not None
and receiver.name in parameter_specs
):
recv_spec = parameter_specs.get(receiver.name)
# An unresolved parameter still 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 != "map":
return None

if method in ("clear", "put_all"):
return PineType.VOID, None
if method == "keys":
return (
PineType.VOID,
TypeSpec.array(recv_spec.key or TypeSpec.primitive("string")),
)
if method == "values":
return (
PineType.VOID,
TypeSpec.array(recv_spec.value or TypeSpec.primitive("float")),
)
if method == "copy":
return PineType.VOID, recv_spec
if (
method in ("put", "get", "remove")
and recv_spec.value is not None
and recv_spec.value.kind == "primitive"
and recv_spec.value.name == "string"
):
return PineType.STRING, None
return None

@staticmethod
def _pine_type_to_spec(pine_type: PineType) -> TypeSpec:
mapping = {
Expand Down
53 changes: 42 additions & 11 deletions pineforge_codegen/codegen/drawing.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
from ..ast_nodes import FuncCall, Identifier, MemberAccess
from .tables import DRAWING_TYPE_TO_CPP, DRAWING_ARENA, ARRAY_VOID_METHODS as _ARRAY_VOID_METHODS

_MAP_VOID_METHODS = frozenset({"clear", "put_all"})

# ---------------------------------------------------------------------------
# Canonical Pine v6 constructor param-name lists (positional order).
# Only the GEOMETRY names are consumed downstream; every other (visual) name is
Expand Down Expand Up @@ -478,26 +480,55 @@ def _call_is_void(self, node) -> bool:
expression and so cannot be used as a function's return value / RHS.

Covers drawing setters/delete/visual-noop (delegated to
``_drawing_call_is_void``) AND the Pine array MUTATOR methods whose C++
``_drawing_call_is_void``), the Pine array MUTATOR methods whose C++
lowering is void / an iterator (``array.push/insert/clear/set/fill/
sort/reverse/concat/unshift``) — a function ending in
``array.clear(x)`` returns void in Pine, so it must emit as a statement
with a default return, not ``return x.clear();``.
sort/reverse/concat/unshift``), and terminal map ``clear``/``put_all``
calls. A function ending in one of these calls must emit it as a
statement with a default return, never ``return <void-expression>;``.
"""
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
_fn, ns = self._resolve_callee(node.callee)
if method not in _ARRAY_VOID_METHODS:
if method in _ARRAY_VOID_METHODS:
# array.<method>(arr, ...) namespace form OR arr.<method>(...)
# 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":
return True

if method not in _MAP_VOID_METHODS:
return False
# array.<method>(arr, ...) namespace form OR arr.<method>(...) method
# form on a std::vector receiver.
if ns == "array":
return True
recv_spec = self._type_spec_from_expr(node.callee.object)
return recv_spec is not None and recv_spec.kind == "array"
# Preserve the unsupported arbitrary-map-receiver boundary. Namespace
# functional calls are established; method calls must use a named
# global/local/parameter receiver.
if ns == "map":
# The established namespace-functional form has a positional map
# receiver. Keyword-only receiver routing remains unsupported
# and must retain its pre-KI-48g fallback output.
expected_arity = 1 if method == "clear" else 2
return len(node.args) == expected_arity and not node.kwargs
if not isinstance(node.callee.object, Identifier):
return False
name = node.callee.object.name
param_spec = getattr(self, "_current_func_param_specs", {}).get(name)
if method == "clear":
if node.args or node.kwargs:
return False
elif param_spec is not None and param_spec.kind == "map":
if not (
(len(node.args) == 1 and not node.kwargs)
or (not node.args and set(node.kwargs) == {"id2"})
):
return False
elif len(node.args) != 1 or node.kwargs:
return False
recv_spec = param_spec or self._type_spec_from_expr(node.callee.object)
return recv_spec is not None and recv_spec.kind == "map"

# ------------------------------------------------------------------
# _uses_drawing detection + arena caps
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 @@ -1048,8 +1048,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:
# Array-returning function (``f() => array.from(...)``) — emit the
# vector type from the inferred element TypeSpec.
# Collection-returning function (array or map terminal) — emit the
# concrete C++ collection type from its inferred TypeSpec.
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
Loading
Loading