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
22 changes: 12 additions & 10 deletions pineforge_codegen/codegen/visit_stmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@
)
from ..symbols import TypeSpec
from .tables import (
SKIP_VAR_TYPES,
TA_RETURNS_BOOL,
TA_TUPLE_FIELDS,
MATRIX_RETURNING_METHODS,
Expand Down Expand Up @@ -356,16 +355,19 @@ def _visit_var_decl(self, node: VarDecl, lines: list[str], pad: str) -> None:
lines.append(f"{pad}{cpp_type} {safe};")
return

# Skip visual function assignments — but still emit declaration for
# table function results since the var may be used later
# Visual/drawing function assignments (line.new, label.new, box.new,
# table.new, ...) are no-ops in a backtest, but the assigned variable may
# still be referenced later (e.g. pushed into an array<line>, or used as a
# handle by sibling set_* calls). Emit a default-valued local declaration
# so those references compile; the value is inert. Global members are
# already declared at class scope, so only locals need this. (Previously
# only `table` results were declared, which dropped loop-local line/label
# handles and produced "use of undeclared identifier".)
if isinstance(node.value, FuncCall) and self._is_skip_expr(node.value):
func_name, namespace = self._resolve_callee(node.value.callee)
if namespace in SKIP_VAR_TYPES:
# Emit var with default value so references don't fail
if not is_global_member:
cpp_type = self._type_for_decl(node)
default = "0" if cpp_type in ("int", "double") else ('std::string("")' if cpp_type == "std::string" else "false")
lines.append(f"{pad}{cpp_type} {safe} = {default};")
if not is_global_member:
cpp_type = self._type_for_decl(node)
default = "0" if cpp_type in ("int", "double") else ('std::string("")' if cpp_type == "std::string" else "false")
lines.append(f"{pad}{cpp_type} {safe} = {default};")
return

# TA call
Expand Down
33 changes: 33 additions & 0 deletions tests/test_codegen_drawing_handles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Regression: loop-local drawing-object handles must be declared.

Drawing calls (line.new / label.new / box.new / table.new ...) are no-ops in a
backtest, but the variable they're assigned to may still be referenced (e.g.
pushed into an `array<line>`). When the assignment lives in a for-loop body the
handle is a local; it must be declared (as an inert default) or the generated
C++ references an undeclared identifier.
"""

from pineforge_codegen import transpile


def _cpp(body: str) -> str:
return transpile('//@version=6\nstrategy("t")\n' + body + "\n")


def test_line_handle_in_loop_is_declared():
cpp = _cpp(
"var a = array.new<line>()\n"
"for k = 1 to 3\n"
" ln = line.new(bar_index, close, bar_index + 1, close)\n"
" array.push(a, ln)"
)
assert "double ln" in cpp or "auto ln" in cpp


def test_label_handle_in_loop_is_declared():
cpp = _cpp(
"for k = 1 to 3\n"
" lb = label.new(bar_index, high, 'x')\n"
" label.delete(lb)"
)
assert "double lb" in cpp or "auto lb" in cpp
Loading