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
110 changes: 110 additions & 0 deletions pineforge_codegen/analyzer/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,18 @@ def _record_global_binding_stmt(self, name: str, pine_type: PineType,
if top_stmt is not None and (not info.source_stmts or info.source_stmts[-1] is not top_stmt):
info.source_stmts.append(top_stmt)

@staticmethod
def _is_input_func_call(node: FuncCall) -> bool:
"""True for an ``input(...)`` or ``input.<member>(...)`` call."""
callee = node.callee
if isinstance(callee, Identifier) and callee.name == "input":
return True
return (
isinstance(callee, MemberAccess)
and isinstance(callee.object, Identifier)
and callee.object.name == "input"
)

def _collect_security_mutable_globals(
self, node: ASTNode | None, resolving: set[str] | None = None
) -> set[str]:
Expand Down Expand Up @@ -388,6 +400,20 @@ def _collect_security_mutable_globals(
resolving.remove(call_key)
return out

if isinstance(node, FuncCall) and self._is_input_func_call(node):
# An ``input.*()`` / ``input()`` initializer is a compile-time
# constant. Only its defval (first positional or ``defval=``) can
# carry a genuine data dependency; the cosmetic kwargs
# (group/tooltip/title/inline/display/confirm/minval/maxval/step)
# are presentation-only. Walking them would falsely pull a
# ``var string GROUP = "..."`` label into the security's
# mutable-globals set and trip the "TA ctor depends on rebound
# mutable globals" reject (parallax / higherTimeframeLength).
defval = node.args[0] if node.args else node.kwargs.get("defval")
if defval is not None:
out |= self._collect_security_mutable_globals(defval, resolving)
return out

def walk(value: Any) -> None:
nonlocal out
if value is None:
Expand Down Expand Up @@ -694,6 +720,83 @@ def _udt_name_from_ctor(self, value: ASTNode) -> str | None:
return None
return owner

def _func_terminal_drawing_type(self, func_node: FuncDef) -> str | None:
"""Resolve the drawing-handle / UDT type of a function's terminal
(return) expression for cases the direct ``_udt_name_from_ctor`` on the
last statement misses:

- the last statement is an ``IfStmt`` whose terminal branch yields a
drawing/UDT constructor (``makeEventLabel`` => ``if cond\\n
label.new(...)``); and
- the last statement is a bare ``Identifier`` bound to a
drawing-handle local (``setTradeLine`` => ``line result = ...`` then
a trailing ``result``).

Returns the drawing/UDT type name, or ``None``. Without this a function
that returns a ``line``/``label`` handle this way is mis-typed
``double`` and clang rejects ``no viable conversion from Line to
double``.
"""
from .types import _DRAWING_TYPE_NAMES

body = func_node.body
if not body:
return None

# Map a drawing-handle local var name -> drawing type. Seeded from
# declared drawing type hints (``line result``) and the function's own
# drawing-typed parameters, plus any local first bound to a drawing
# ``<ns>.new(...)`` constructor.
local_drawing: dict[str, str] = {}
param_hints = (func_node.annotations or {}).get("param_type_hints", [])
for i, p in enumerate(func_node.params):
hint = param_hints[i] if i < len(param_hints) else None
if hint in _DRAWING_TYPE_NAMES:
local_drawing[p] = hint

def _scan(stmts):
for st in stmts:
if isinstance(st, VarDecl):
if st.type_hint in _DRAWING_TYPE_NAMES:
local_drawing[st.name] = st.type_hint
else:
dt = self._udt_name_from_ctor(st.value)
if dt in _DRAWING_TYPE_NAMES:
local_drawing.setdefault(st.name, dt)
elif isinstance(st, Assignment) and isinstance(st.target, Identifier):
dt = self._udt_name_from_ctor(st.value)
if dt in _DRAWING_TYPE_NAMES:
local_drawing.setdefault(st.target.name, dt)
elif isinstance(st, IfStmt):
_scan(st.body)
_scan(st.else_body)

_scan(body)

def _resolve_terminal(stmt):
# An if used as the function's return expression: the value is the
# terminal of the executed branch — recurse into the body's (then
# else's) terminal statement.
if isinstance(stmt, IfStmt):
for branch in (stmt.body, stmt.else_body):
if branch:
t = _resolve_terminal(branch[-1])
if t is not None:
return t
return None
expr = None
if isinstance(stmt, ExprStmt):
expr = stmt.expr
elif not isinstance(stmt, TupleLiteral) and hasattr(stmt, "loc"):
expr = stmt
if expr is None:
return None
if isinstance(expr, Identifier) and expr.name in local_drawing:
return local_drawing[expr.name]
return self._udt_name_from_ctor(expr)

return _resolve_terminal(body[-1])

def _visit_VarDecl(self, node: VarDecl) -> PineType:
# Infer type from the value expression
val_type = self._visit(node.value)
Expand Down Expand Up @@ -1015,6 +1118,13 @@ def _visit_FuncDef(self, node: FuncDef) -> PineType:
# last_stmt is itself an expression node (single-expr funcs)
ret_expr = last_stmt if hasattr(last_stmt, "loc") else None
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
# branch (``makeEventLabel``) or returned as a bare drawing-handle
# local (``setTradeLine``) are not direct ctors on the last
# expression — resolve them so the function emits the C++ handle
# type (Line/Label/...) instead of the ``double`` default.
udt_ret = self._func_terminal_drawing_type(node)
if udt_ret is not None:
self._func_udt_return_types[node.name] = udt_ret
# Array-return inference: a function whose body ends in
Expand Down
28 changes: 28 additions & 0 deletions pineforge_codegen/codegen/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,29 @@ def __init__(self, ctx: AnalyzerContext) -> None:
self._map_vars.add(_name)
elif _spec.kind == "udt" and _spec.name:
self._udt_var_types.setdefault(_name, _spec.name)
# Table / polyline variables and params have NO C++ representation
# (SKIP_VAR_TYPES). A *method* call on such a receiver
# (``panel.cell(...)``, ``dash.merge_cells(...)``) is a visual no-op
# that must be dropped — but unlike the namespace form
# (``table.cell(...)``) the receiver is a bare var/param the
# namespace-based skip cannot see. Collect those names so
# ``_is_skip_expr`` can drop their method calls.
_SKIP_DECL_TYPES = set(SKIP_VAR_TYPES) | {"polyline"}
self._visual_drop_vars: set[str] = set()
for _node in self._walk_ast(self.ctx.ast):
if isinstance(_node, VarDecl):
if _node.type_hint in _SKIP_DECL_TYPES:
self._visual_drop_vars.add(_node.name)
elif isinstance(_node.value, FuncCall):
_fn, _ns = self._resolve_callee(_node.value.callee)
if _fn == "new" and _ns in _SKIP_DECL_TYPES:
self._visual_drop_vars.add(_node.name)
elif isinstance(_node, (FuncDef, MethodDef)):
_hints = (getattr(_node, "annotations", None) or {}).get("param_type_hints") or []
for _i, _p in enumerate(getattr(_node, "params", []) or []):
_h = _hints[_i] if _i < len(_hints) else None
if _h and str(_h).replace(" ", "") in _SKIP_DECL_TYPES:
self._visual_drop_vars.add(_p)
# Collect request.security metadata per call
self._security_eval_info: list[dict] = []
self._security_ta_variant_names: dict[tuple[int, int, tuple], str] = {}
Expand Down Expand Up @@ -1735,6 +1758,11 @@ def _is_skip_expr(self, node) -> bool:
return True
if namespace in SKIP_VAR_TYPES:
return True
# Method call on a table/polyline-typed receiver var/param
# (``panel.cell(...)``). These types have no C++ representation, so
# the call is a visual no-op — drop it (mirrors the namespace form).
if namespace in self._visual_drop_vars:
return True
# strategy.risk.* — handled in _visit_stmt, not skipped
if isinstance(node, MemberAccess):
if isinstance(node.object, Identifier) and node.object.name in SKIP_NAMESPACES:
Expand Down
32 changes: 19 additions & 13 deletions pineforge_codegen/codegen/emit_top.py
Original file line number Diff line number Diff line change
Expand Up @@ -908,22 +908,28 @@ def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | No
else:
for i, s in enumerate(node.body):
if i == len(node.body) - 1 and isinstance(s, ExprStmt):
# A void drawing setter / delete / visual-noop used as the
# last statement cannot be the return value (it lowers to a
# void C++ call). Emit it as a plain statement and let the
# default-return path below supply the function's result.
if self._call_is_void(s.expr):
# A void drawing setter / delete / visual-noop, or a dropped
# table/polyline method call (``panel.cell(...)``), used as
# the last statement cannot be the return value (it lowers to
# a void / no-op C++ call). Emit it as a plain statement
# (which ``_is_skip_expr`` drops) and let the default-return
# path below supply the function's result.
if self._call_is_void(s.expr) or self._is_skip_expr(s.expr):
self._visit_stmt(s, lines, indent=2)
else:
lines.append(f" return {self._visit_expr(s.expr)};")
emitted_return = True
elif i == len(node.body) - 1 and isinstance(s, (SwitchStmt, IfStmt)):
# Switch/if as last statement = return expression in PineScript
# Emit as: double _ret = 0; if/switch assigns _ret; return _ret;
default_ret = (
f"{ret_type}{{}}" if ret_type in self._udt_defs
else self._default_for_type(ret_type)
)
# A drawing-handle / UDT return type must brace-init its
# default (``Label _func_ret = Label{};``) — falling through
# to ``_default_for_type`` would emit ``0.0`` and clang would
# reject ``Label _func_ret = 0.0;``.
if ret_type in self._udt_defs or ret_type in DRAWING_TYPE_TO_CPP.values():
default_ret = f"{ret_type}{{}}"
else:
default_ret = self._default_for_type(ret_type)
lines.append(f" {ret_type} _func_ret = {default_ret};")
self._visit_if_switch_expr(s, "_func_ret", lines, indent=2)
lines.append(f" return _func_ret;")
Expand All @@ -938,10 +944,10 @@ def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | No
default_vals = ", ".join(["0.0"] * fi.tuple_element_count)
lines.append(f" return std::make_tuple({default_vals});")
else:
default_ret = (
f"{ret_type}{{}}" if ret_type in self._udt_defs
else self._default_for_type(ret_type)
)
if ret_type in self._udt_defs or ret_type in DRAWING_TYPE_TO_CPP.values():
default_ret = f"{ret_type}{{}}"
else:
default_ret = self._default_for_type(ret_type)
lines.append(f" return {default_ret};")

lines.append(" }")
Expand Down
11 changes: 8 additions & 3 deletions pineforge_codegen/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,13 +596,17 @@ def _parse_param_type_annotation(self) -> str | None:
self._advance()
# Is there a type annotation before the parameter name? A builtin type
# token always is; an IDENT is a type only if followed by another IDENT
# (``line ln``) or by ``[`` (``line[] arr``).
# (``line ln``), by ``[`` (``line[] arr``), or by ``<`` (the generic
# collection syntax ``array<float> xs`` / ``matrix<float> m`` /
# ``map<string,float> mp``). Without the ``<`` case the generic type is
# mis-consumed as the parameter name and the whole function definition
# silently fails to parse (its body leaks to top-level scope).
has_type = False
if self._current().type in TYPE_TOKENS:
has_type = True
elif self._check(TokenType.IDENT):
nxt = self._peek().type
if nxt == TokenType.IDENT or nxt == TokenType.LBRACKET:
if nxt in (TokenType.IDENT, TokenType.LBRACKET, TokenType.LT):
has_type = True
if not has_type:
return None
Expand Down Expand Up @@ -729,7 +733,8 @@ def _parse_method_def(self):
if self._current().type in TYPE_KEYWORDS:
param_type = self._parse_type_hint_string()
elif (self._current().type == TokenType.IDENT
and self._peek().type == TokenType.IDENT):
and self._peek().type in (TokenType.IDENT, TokenType.LBRACKET, TokenType.LT)):
# ``line ln`` / ``float[] arr`` / ``array<float> xs`` typed param.
param_type = self._parse_type_hint_string()
p = self._consume(TokenType.IDENT).value
pdefault = None
Expand Down
Loading
Loading