pineforge-codegen 0.6.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pineforge_codegen/__init__.py +53 -0
- pineforge_codegen/analyzer/__init__.py +60 -0
- pineforge_codegen/analyzer/base.py +1563 -0
- pineforge_codegen/analyzer/call_handlers.py +895 -0
- pineforge_codegen/analyzer/contracts.py +163 -0
- pineforge_codegen/analyzer/diagnostics.py +118 -0
- pineforge_codegen/analyzer/tables.py +204 -0
- pineforge_codegen/analyzer/types.py +250 -0
- pineforge_codegen/ast_nodes.py +293 -0
- pineforge_codegen/codegen/__init__.py +78 -0
- pineforge_codegen/codegen/base.py +1381 -0
- pineforge_codegen/codegen/emit_top.py +875 -0
- pineforge_codegen/codegen/helpers.py +163 -0
- pineforge_codegen/codegen/helpers_syminfo.py +134 -0
- pineforge_codegen/codegen/input.py +189 -0
- pineforge_codegen/codegen/security.py +1564 -0
- pineforge_codegen/codegen/ta.py +298 -0
- pineforge_codegen/codegen/tables.py +613 -0
- pineforge_codegen/codegen/types.py +573 -0
- pineforge_codegen/codegen/visit_call.py +1305 -0
- pineforge_codegen/codegen/visit_expr.py +701 -0
- pineforge_codegen/codegen/visit_stmt.py +729 -0
- pineforge_codegen/errors.py +98 -0
- pineforge_codegen/lexer.py +531 -0
- pineforge_codegen/parser.py +1198 -0
- pineforge_codegen/pragmas.py +117 -0
- pineforge_codegen/signatures.py +808 -0
- pineforge_codegen/support_checker.py +1111 -0
- pineforge_codegen/symbols.py +118 -0
- pineforge_codegen/tokens.py +406 -0
- pineforge_codegen/tv_input_choices.py +86 -0
- pineforge_codegen-0.6.5.dist-info/METADATA +462 -0
- pineforge_codegen-0.6.5.dist-info/RECORD +35 -0
- pineforge_codegen-0.6.5.dist-info/WHEEL +4 -0
- pineforge_codegen-0.6.5.dist-info/licenses/LICENSE +197 -0
|
@@ -0,0 +1,1563 @@
|
|
|
1
|
+
"""Semantic analyzer for PineScript v6 AST.
|
|
2
|
+
|
|
3
|
+
Walks the AST produced by the parser, builds a symbol table, infers types,
|
|
4
|
+
detects series variables, collects TA call-sites, and outputs an
|
|
5
|
+
AnalyzerContext for the code generator.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..ast_nodes import (
|
|
13
|
+
ASTNode,
|
|
14
|
+
Program, StrategyDecl, ImportStmt,
|
|
15
|
+
VarDecl, Assignment, TupleAssign,
|
|
16
|
+
IfStmt, ForStmt, ForInStmt, WhileStmt, SwitchStmt, BreakStmt, ContinueStmt,
|
|
17
|
+
FuncDef, ExprStmt,
|
|
18
|
+
BinOp, UnaryOp, Ternary, FuncCall, Subscript,
|
|
19
|
+
Identifier, MemberAccess, TypeAnnotation,
|
|
20
|
+
NumberLiteral, StringLiteral, BoolLiteral, NaLiteral, ColorLiteral,
|
|
21
|
+
TupleLiteral,
|
|
22
|
+
TypeDecl, EnumDecl, MethodDef, TypeField,
|
|
23
|
+
)
|
|
24
|
+
from ..symbols import PineType, Symbol, SymbolTable, TypeSpec
|
|
25
|
+
from ..errors import SourceLocation, Diagnostic, CompileError, Level, Phase
|
|
26
|
+
from .. import signatures as sigs
|
|
27
|
+
from .. import tv_input_choices as tv_in
|
|
28
|
+
# Output dataclasses (contract with the codegen) live in contracts.py so
|
|
29
|
+
# the import graph stays a strict DAG: contracts <- {base, call_handlers,
|
|
30
|
+
# __init__}.
|
|
31
|
+
from .contracts import (
|
|
32
|
+
AnalyzerContext,
|
|
33
|
+
FixnanCallSite,
|
|
34
|
+
FuncInfo,
|
|
35
|
+
MutableGlobalInfo,
|
|
36
|
+
SecurityCallInfo,
|
|
37
|
+
TACallSite,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Output data structures (defined in contracts.py; re-imported above so
|
|
43
|
+
# this module's existing references like ``TACallSite(...)`` resolve
|
|
44
|
+
# without qualification).
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# AnalyzerContext is defined in ``contracts.py`` (re-imported above).
|
|
49
|
+
# Adding new context fields: edit ``contracts.py``, not this file.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Mapping tables — definitions live in ``tables.py``; re-imported here so
|
|
54
|
+
# inline references inside this module (TA_CLASS_MAP[name], BUILTIN_VARS,
|
|
55
|
+
# etc.) keep resolving without qualification. The package-level
|
|
56
|
+
# ``__init__.py`` re-exports the same names for external consumers
|
|
57
|
+
# (``codegen/base.py``, ``support_checker.py``, and external tests).
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
from .tables import (
|
|
61
|
+
TA_CLASS_MAP,
|
|
62
|
+
TA_PERIOD_ARG,
|
|
63
|
+
TA_TUPLE_RETURNS,
|
|
64
|
+
TA_MULTI_CTOR,
|
|
65
|
+
TA_NO_CTOR,
|
|
66
|
+
BUILTIN_VARS,
|
|
67
|
+
BAR_FIELDS,
|
|
68
|
+
SKIP_FUNCS,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# TypeHelper mixin owns the Pine type-hint / expression -> TypeSpec / PineType
|
|
72
|
+
# inference helpers previously inlined in this module; see
|
|
73
|
+
# ``compiler/transpiler/analyzer/types.py``.
|
|
74
|
+
from .types import TypeHelper
|
|
75
|
+
|
|
76
|
+
# DiagnosticsHelper mixin owns _error / _warn / _input_diag_loc /
|
|
77
|
+
# _expr_to_str / _warn_if_unknown_source_id; see
|
|
78
|
+
# ``compiler/transpiler/analyzer/diagnostics.py``.
|
|
79
|
+
from .diagnostics import DiagnosticsHelper
|
|
80
|
+
|
|
81
|
+
# CallHandlers mixin owns the ~14 _handle_*_call dispatchers plus
|
|
82
|
+
# input-validation / TA-arg-merge helpers; see
|
|
83
|
+
# ``compiler/transpiler/analyzer/call_handlers.py``. Largest analyzer
|
|
84
|
+
# mixin (~500 lines).
|
|
85
|
+
from .call_handlers import CallHandlers
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Analyzer
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
93
|
+
"""Semantic analysis pass over PineScript v6 AST.
|
|
94
|
+
|
|
95
|
+
Mixin chain (Python MRO is left-to-right; method names are
|
|
96
|
+
intentionally kept disjoint across mixins so the order is mostly
|
|
97
|
+
cosmetic):
|
|
98
|
+
* ``CallHandlers`` -- per-callee dispatch and bookkeeping for
|
|
99
|
+
``ta.*`` / ``request.*`` / ``strategy.*`` / ``input.*`` /
|
|
100
|
+
``fixnan(...)`` / user-function calls (``_handle_*_call``,
|
|
101
|
+
``_merge_ta_args``, ``_merge_input_params``,
|
|
102
|
+
``_validate_input_member_tv``, ...)
|
|
103
|
+
* ``DiagnosticsHelper`` -- ``_error`` / ``_warn`` /
|
|
104
|
+
``_input_diag_loc`` / ``_expr_to_str`` /
|
|
105
|
+
``_warn_if_unknown_source_id``
|
|
106
|
+
* ``TypeHelper`` -- Pine type-hint / expression -> TypeSpec /
|
|
107
|
+
PineType inference helpers (``_type_spec_from_hint``,
|
|
108
|
+
``_type_spec_from_expr``, ``_extract_literal_value``, ...)
|
|
109
|
+
|
|
110
|
+
Future steps may further peel visitor / top-level / UDT mixins
|
|
111
|
+
out of this class; see ``compiler/transpiler/analyzer/`` for
|
|
112
|
+
the package layout.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(self, ast: Program, filename: str = "<stdin>") -> None:
|
|
116
|
+
self._ast = ast
|
|
117
|
+
self._filename = filename
|
|
118
|
+
self._symbols = SymbolTable()
|
|
119
|
+
self._ta_call_sites: list[TACallSite] = []
|
|
120
|
+
self._series_vars: set[str] = set()
|
|
121
|
+
self._series_bar_fields: set[str] = set()
|
|
122
|
+
self._var_members: list[tuple[str, PineType, str]] = []
|
|
123
|
+
self._func_infos: list[FuncInfo] = []
|
|
124
|
+
self._fixnan_sites: list[FixnanCallSite] = []
|
|
125
|
+
self._strategy_params: dict = {}
|
|
126
|
+
self._diagnostics: list[Diagnostic] = []
|
|
127
|
+
self._global_var_decls: list[tuple[str, PineType]] = []
|
|
128
|
+
self._global_expr_map: dict[str, Any] = {}
|
|
129
|
+
self._var_member_init_exprs: dict[str, Any] = {}
|
|
130
|
+
self._ta_counter = 0
|
|
131
|
+
self._fixnan_counter = 0
|
|
132
|
+
# Track user-defined function nodes for deferred analysis
|
|
133
|
+
self._func_defs: dict[str, FuncDef] = {}
|
|
134
|
+
# Track user-defined function return types
|
|
135
|
+
self._func_return_types: dict[str, PineType] = {}
|
|
136
|
+
# Track user-defined function tuple returns
|
|
137
|
+
self._func_returns_tuple: dict[str, bool] = {}
|
|
138
|
+
self._func_tuple_element_count: dict[str, int] = {}
|
|
139
|
+
# Track user-defined functions whose body returns a UDT instance —
|
|
140
|
+
# maps func_name -> UDT type name. Detected from the body's final
|
|
141
|
+
# expression (``=> Sample.new(...)`` or last stmt ``Sample.new(...)``).
|
|
142
|
+
# Probe: data/validation/udt-method-probe-20-udt-return-from-func.
|
|
143
|
+
self._func_udt_return_types: dict[str, str] = {}
|
|
144
|
+
# Per-function var_members and series_vars (for call-site cloning)
|
|
145
|
+
self._func_var_members: dict[str, list] = {} # func_name -> [(name, PineType, init_str)]
|
|
146
|
+
self._func_series_vars: dict[str, set] = {} # func_name -> set[str]
|
|
147
|
+
# Per-call-site TA tracking for user functions
|
|
148
|
+
self._func_ta_ranges: dict[str, tuple[int, int]] = {} # func_name -> (start, end) indices
|
|
149
|
+
self._func_call_site_count: dict[str, int] = {} # func_name -> count
|
|
150
|
+
self._func_call_cs_map: dict[int, tuple[str, int]] = {} # call_node_id -> (func_name, cs_idx)
|
|
151
|
+
# UDT field definitions: type_name -> {field_name: PineType}
|
|
152
|
+
self._udt_fields: dict[str, dict[str, PineType]] = {}
|
|
153
|
+
# var_name -> UDT type for variables holding UDT instances
|
|
154
|
+
self._udt_var_types: dict[str, str] = {}
|
|
155
|
+
self._collection_types: dict[str, TypeSpec] = {}
|
|
156
|
+
self._udt_field_type_specs: dict[str, dict[str, TypeSpec]] = {}
|
|
157
|
+
# Enum definitions: enum_name -> list of member names
|
|
158
|
+
self._enum_defs: dict[str, list[str]] = {}
|
|
159
|
+
self._enum_member_strings: dict[str, list[str]] = {}
|
|
160
|
+
# request.security rebinding helpers for mutable globals
|
|
161
|
+
self._global_binding_infos: dict[str, MutableGlobalInfo] = {}
|
|
162
|
+
self._global_reassigned_names: set[str] = set()
|
|
163
|
+
self._current_top_level_stmt: ASTNode | None = None
|
|
164
|
+
self._global_scope = True
|
|
165
|
+
self._static_vars: set[str] = set()
|
|
166
|
+
|
|
167
|
+
# Pre-populate builtins
|
|
168
|
+
self._populate_builtins()
|
|
169
|
+
|
|
170
|
+
def _populate_builtins(self) -> None:
|
|
171
|
+
"""Add built-in PineScript variables to the global scope."""
|
|
172
|
+
dummy_loc = SourceLocation(file=self._filename, line=0, col=0, end_col=0)
|
|
173
|
+
for name, ptype in BUILTIN_VARS.items():
|
|
174
|
+
sym = Symbol(
|
|
175
|
+
name=name,
|
|
176
|
+
pine_type=ptype,
|
|
177
|
+
is_series=name in BAR_FIELDS,
|
|
178
|
+
is_var=False,
|
|
179
|
+
is_const=False,
|
|
180
|
+
const_value=None,
|
|
181
|
+
scope="global",
|
|
182
|
+
loc=dummy_loc,
|
|
183
|
+
)
|
|
184
|
+
self._symbols.define(sym)
|
|
185
|
+
|
|
186
|
+
def _ensure_pine_v6(self) -> None:
|
|
187
|
+
"""PineForge implements PineScript v6 only; reject other versions or missing directive."""
|
|
188
|
+
if self._ast.version is None:
|
|
189
|
+
loc = SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
190
|
+
raise CompileError(
|
|
191
|
+
[
|
|
192
|
+
Diagnostic(
|
|
193
|
+
level=Level.ERROR,
|
|
194
|
+
phase=Phase.ANALYZER,
|
|
195
|
+
location=loc,
|
|
196
|
+
message=(
|
|
197
|
+
"Missing PineScript version directive. "
|
|
198
|
+
"PineForge supports PineScript v6 only."
|
|
199
|
+
),
|
|
200
|
+
hint='Add //@version=6 as the first line of your script.',
|
|
201
|
+
)
|
|
202
|
+
]
|
|
203
|
+
)
|
|
204
|
+
if self._ast.version != 6:
|
|
205
|
+
loc = SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
206
|
+
raise CompileError(
|
|
207
|
+
[
|
|
208
|
+
Diagnostic(
|
|
209
|
+
level=Level.ERROR,
|
|
210
|
+
phase=Phase.ANALYZER,
|
|
211
|
+
location=loc,
|
|
212
|
+
message=(
|
|
213
|
+
f"PineForge supports PineScript v6 only (found //@version={self._ast.version})."
|
|
214
|
+
),
|
|
215
|
+
hint="Migrate the script to //@version=6 using TradingView's editor.",
|
|
216
|
+
)
|
|
217
|
+
]
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
# Public entry point
|
|
222
|
+
# ------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def analyze(self) -> AnalyzerContext:
|
|
225
|
+
"""Run semantic analysis and return the analyzer context."""
|
|
226
|
+
self._ensure_pine_v6()
|
|
227
|
+
self._visit(self._ast)
|
|
228
|
+
|
|
229
|
+
# Propagate call-site counts to sub-functions called within
|
|
230
|
+
# multi-call-site functions. If f() has N call sites and calls g()
|
|
231
|
+
# internally, g() also needs N call-site variants so each f_csK
|
|
232
|
+
# can call g_csK with isolated state.
|
|
233
|
+
self._propagate_call_site_counts()
|
|
234
|
+
|
|
235
|
+
# Keep only truly pure global expressions for request.security rebinding.
|
|
236
|
+
# Globals later reassigned with := become series/stateful variables and
|
|
237
|
+
# must not be rebound to their declaration-time initializer.
|
|
238
|
+
for name, info in self._global_binding_infos.items():
|
|
239
|
+
info.is_series = name in self._series_vars
|
|
240
|
+
|
|
241
|
+
mutable_global_infos = {
|
|
242
|
+
name: info
|
|
243
|
+
for name, info in self._global_binding_infos.items()
|
|
244
|
+
if info.is_var or name in self._global_reassigned_names
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
pure_global_expr_map = {
|
|
248
|
+
k: v for k, v in self._global_expr_map.items() if k not in mutable_global_infos
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return AnalyzerContext(
|
|
252
|
+
ast=self._ast,
|
|
253
|
+
symbols=self._symbols,
|
|
254
|
+
ta_call_sites=self._ta_call_sites,
|
|
255
|
+
series_vars=self._series_vars,
|
|
256
|
+
series_bar_fields=self._series_bar_fields,
|
|
257
|
+
var_members=self._var_members,
|
|
258
|
+
func_infos=self._func_infos,
|
|
259
|
+
fixnan_sites=self._fixnan_sites,
|
|
260
|
+
strategy_params=self._strategy_params,
|
|
261
|
+
diagnostics=self._diagnostics,
|
|
262
|
+
filename=self._filename,
|
|
263
|
+
global_var_decls=self._global_var_decls,
|
|
264
|
+
global_expr_map=pure_global_expr_map,
|
|
265
|
+
var_member_init_exprs=self._var_member_init_exprs,
|
|
266
|
+
func_ta_ranges=self._func_ta_ranges,
|
|
267
|
+
func_call_cs_map=self._func_call_cs_map,
|
|
268
|
+
func_call_site_counts=self._func_call_site_count,
|
|
269
|
+
udt_defs=self._udt_fields,
|
|
270
|
+
enum_defs=self._enum_defs,
|
|
271
|
+
enum_member_strings=self._enum_member_strings,
|
|
272
|
+
security_calls=getattr(self, "_security_calls", []),
|
|
273
|
+
global_mutable_infos=mutable_global_infos,
|
|
274
|
+
func_var_members=self._func_var_members,
|
|
275
|
+
func_series_vars=self._func_series_vars,
|
|
276
|
+
udt_var_types=dict(self._udt_var_types),
|
|
277
|
+
collection_types=dict(self._collection_types),
|
|
278
|
+
udt_field_type_specs=dict(self._udt_field_type_specs),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def _record_global_binding_stmt(self, name: str, pine_type: PineType,
|
|
282
|
+
is_var: bool, decl_node: ASTNode | None = None) -> None:
|
|
283
|
+
info = self._global_binding_infos.get(name)
|
|
284
|
+
if info is None:
|
|
285
|
+
info = MutableGlobalInfo(
|
|
286
|
+
name=name,
|
|
287
|
+
pine_type=pine_type,
|
|
288
|
+
is_var=is_var,
|
|
289
|
+
decl_node=decl_node,
|
|
290
|
+
)
|
|
291
|
+
self._global_binding_infos[name] = info
|
|
292
|
+
else:
|
|
293
|
+
info.pine_type = pine_type
|
|
294
|
+
info.is_var = info.is_var or is_var
|
|
295
|
+
if decl_node is not None and info.decl_node is None:
|
|
296
|
+
info.decl_node = decl_node
|
|
297
|
+
|
|
298
|
+
top_stmt = self._current_top_level_stmt or decl_node
|
|
299
|
+
if top_stmt is not None and (not info.source_stmts or info.source_stmts[-1] is not top_stmt):
|
|
300
|
+
info.source_stmts.append(top_stmt)
|
|
301
|
+
|
|
302
|
+
def _collect_security_mutable_globals(
|
|
303
|
+
self, node: ASTNode | None, resolving: set[str] | None = None
|
|
304
|
+
) -> set[str]:
|
|
305
|
+
if node is None:
|
|
306
|
+
return set()
|
|
307
|
+
if resolving is None:
|
|
308
|
+
resolving = set()
|
|
309
|
+
|
|
310
|
+
out: set[str] = set()
|
|
311
|
+
|
|
312
|
+
if isinstance(node, Identifier):
|
|
313
|
+
name = node.name
|
|
314
|
+
if name in self._global_binding_infos:
|
|
315
|
+
info = self._global_binding_infos[name]
|
|
316
|
+
if info.is_var or name in self._global_reassigned_names:
|
|
317
|
+
out.add(name)
|
|
318
|
+
if name in resolving:
|
|
319
|
+
return out
|
|
320
|
+
resolving.add(name)
|
|
321
|
+
for stmt in info.source_stmts:
|
|
322
|
+
out |= self._collect_security_mutable_globals(stmt, resolving)
|
|
323
|
+
resolving.remove(name)
|
|
324
|
+
return out
|
|
325
|
+
if name in self._global_expr_map and name not in resolving:
|
|
326
|
+
resolving.add(name)
|
|
327
|
+
out |= self._collect_security_mutable_globals(self._global_expr_map[name], resolving)
|
|
328
|
+
resolving.remove(name)
|
|
329
|
+
return out
|
|
330
|
+
|
|
331
|
+
if isinstance(node, FuncCall) and isinstance(node.callee, Identifier):
|
|
332
|
+
func_name = node.callee.name
|
|
333
|
+
if func_name in self._func_defs:
|
|
334
|
+
call_key = f"func:{func_name}"
|
|
335
|
+
if call_key in resolving:
|
|
336
|
+
return out
|
|
337
|
+
resolving.add(call_key)
|
|
338
|
+
for arg in node.args:
|
|
339
|
+
out |= self._collect_security_mutable_globals(arg, resolving)
|
|
340
|
+
for value in node.kwargs.values():
|
|
341
|
+
out |= self._collect_security_mutable_globals(value, resolving)
|
|
342
|
+
for stmt in self._func_defs[func_name].body:
|
|
343
|
+
out |= self._collect_security_mutable_globals(stmt, resolving)
|
|
344
|
+
resolving.remove(call_key)
|
|
345
|
+
return out
|
|
346
|
+
|
|
347
|
+
def walk(value: Any) -> None:
|
|
348
|
+
nonlocal out
|
|
349
|
+
if value is None:
|
|
350
|
+
return
|
|
351
|
+
if hasattr(value, "__dict__"):
|
|
352
|
+
out |= self._collect_security_mutable_globals(value, resolving)
|
|
353
|
+
return
|
|
354
|
+
if isinstance(value, (list, tuple)):
|
|
355
|
+
for item in value:
|
|
356
|
+
walk(item)
|
|
357
|
+
return
|
|
358
|
+
if isinstance(value, dict):
|
|
359
|
+
for item in value.values():
|
|
360
|
+
walk(item)
|
|
361
|
+
|
|
362
|
+
for child in vars(node).values():
|
|
363
|
+
walk(child)
|
|
364
|
+
|
|
365
|
+
return out
|
|
366
|
+
|
|
367
|
+
def _propagate_call_site_counts(self) -> None:
|
|
368
|
+
"""Propagate call-site counts from parent functions to sub-functions.
|
|
369
|
+
|
|
370
|
+
If function F has N call sites and calls sub-function G internally,
|
|
371
|
+
G also needs N variants so that F_csK can call G_csK with isolated state.
|
|
372
|
+
This ensures every stateful sub-function gets per-call-site isolation
|
|
373
|
+
inherited from its parent.
|
|
374
|
+
"""
|
|
375
|
+
from pineforge_codegen.ast_nodes import FuncCall, FuncDef, Identifier
|
|
376
|
+
|
|
377
|
+
# Collect all user function definitions from AST
|
|
378
|
+
func_defs: dict[str, FuncDef] = {}
|
|
379
|
+
for stmt in self._ast.body:
|
|
380
|
+
if isinstance(stmt, FuncDef):
|
|
381
|
+
func_defs[stmt.name] = stmt
|
|
382
|
+
|
|
383
|
+
# Find which functions call which sub-functions (direct calls only)
|
|
384
|
+
def _find_calls(node, known_funcs: set[str]) -> set[str]:
|
|
385
|
+
calls: set[str] = set()
|
|
386
|
+
if isinstance(node, FuncCall) and isinstance(node.callee, Identifier):
|
|
387
|
+
if node.callee.name in known_funcs:
|
|
388
|
+
calls.add(node.callee.name)
|
|
389
|
+
for attr_val in vars(node).values():
|
|
390
|
+
if isinstance(attr_val, list):
|
|
391
|
+
for item in attr_val:
|
|
392
|
+
if hasattr(item, '__dict__'):
|
|
393
|
+
calls |= _find_calls(item, known_funcs)
|
|
394
|
+
elif hasattr(attr_val, '__dict__'):
|
|
395
|
+
calls |= _find_calls(attr_val, known_funcs)
|
|
396
|
+
return calls
|
|
397
|
+
|
|
398
|
+
known_func_names = set(func_defs.keys())
|
|
399
|
+
|
|
400
|
+
# For each multi-call-site function, propagate count to sub-functions
|
|
401
|
+
# that have stateful locals (series vars or var members)
|
|
402
|
+
changed = True
|
|
403
|
+
while changed:
|
|
404
|
+
changed = False
|
|
405
|
+
for fname, count in list(self._func_call_site_count.items()):
|
|
406
|
+
if count <= 1:
|
|
407
|
+
continue
|
|
408
|
+
if fname not in func_defs:
|
|
409
|
+
continue
|
|
410
|
+
sub_calls = _find_calls(func_defs[fname], known_func_names)
|
|
411
|
+
for sub in sub_calls:
|
|
412
|
+
has_state = (sub in self._func_series_vars or
|
|
413
|
+
sub in self._func_var_members)
|
|
414
|
+
if not has_state:
|
|
415
|
+
continue
|
|
416
|
+
current = self._func_call_site_count.get(sub, 0)
|
|
417
|
+
if current < count:
|
|
418
|
+
self._func_call_site_count[sub] = count
|
|
419
|
+
changed = True
|
|
420
|
+
|
|
421
|
+
def _is_static_expression(self, node: ASTNode | None) -> bool:
|
|
422
|
+
if node is None:
|
|
423
|
+
return True
|
|
424
|
+
|
|
425
|
+
if isinstance(node, (NumberLiteral, StringLiteral, BoolLiteral, NaLiteral, ColorLiteral)):
|
|
426
|
+
return True
|
|
427
|
+
|
|
428
|
+
if isinstance(node, Identifier):
|
|
429
|
+
if node.name in ("open", "high", "low", "close", "volume", "hl2", "hlc3", "ohlc4", "time", "time_close"):
|
|
430
|
+
return True
|
|
431
|
+
if node.name in self._static_vars:
|
|
432
|
+
return True
|
|
433
|
+
sym = self._symbols.resolve(node.name)
|
|
434
|
+
if sym is not None:
|
|
435
|
+
if sym.is_const or node.name.startswith("input"):
|
|
436
|
+
return True
|
|
437
|
+
if getattr(sym, "is_static_series", False):
|
|
438
|
+
return True
|
|
439
|
+
return False
|
|
440
|
+
|
|
441
|
+
if isinstance(node, MemberAccess):
|
|
442
|
+
if isinstance(node.object, Identifier):
|
|
443
|
+
if node.object.name.startswith("input") or node.object.name in self._enum_defs:
|
|
444
|
+
return True
|
|
445
|
+
return self._is_static_expression(node.object)
|
|
446
|
+
|
|
447
|
+
if isinstance(node, BinOp):
|
|
448
|
+
return self._is_static_expression(node.left) and self._is_static_expression(node.right)
|
|
449
|
+
|
|
450
|
+
if isinstance(node, UnaryOp):
|
|
451
|
+
return self._is_static_expression(node.operand)
|
|
452
|
+
|
|
453
|
+
if isinstance(node, Ternary):
|
|
454
|
+
return (self._is_static_expression(node.condition) and
|
|
455
|
+
self._is_static_expression(node.true_val) and
|
|
456
|
+
self._is_static_expression(node.false_val))
|
|
457
|
+
|
|
458
|
+
if isinstance(node, Subscript):
|
|
459
|
+
return self._is_static_expression(node.object) and self._is_static_expression(node.index)
|
|
460
|
+
|
|
461
|
+
if isinstance(node, TupleLiteral):
|
|
462
|
+
return all(self._is_static_expression(elem) for elem in node.elements)
|
|
463
|
+
|
|
464
|
+
if isinstance(node, FuncCall):
|
|
465
|
+
if isinstance(node.callee, MemberAccess) and isinstance(node.callee.object, Identifier):
|
|
466
|
+
ns = node.callee.object.name
|
|
467
|
+
if ns in ("math", "str", "color"):
|
|
468
|
+
return all(self._is_static_expression(arg) for arg in node.args)
|
|
469
|
+
return False
|
|
470
|
+
|
|
471
|
+
return False
|
|
472
|
+
|
|
473
|
+
def _get_target_base_name(self, target: ASTNode) -> str | None:
|
|
474
|
+
if isinstance(target, Identifier):
|
|
475
|
+
return target.name
|
|
476
|
+
if isinstance(target, MemberAccess):
|
|
477
|
+
return self._get_target_base_name(target.object)
|
|
478
|
+
if isinstance(target, Subscript):
|
|
479
|
+
return self._get_target_base_name(target.object)
|
|
480
|
+
return None
|
|
481
|
+
|
|
482
|
+
# ------------------------------------------------------------------
|
|
483
|
+
# Visitor dispatch
|
|
484
|
+
# ------------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
def _visit(self, node: ASTNode | None) -> PineType:
|
|
487
|
+
"""Dispatch to the appropriate visitor and return the inferred type."""
|
|
488
|
+
if node is None:
|
|
489
|
+
return PineType.VOID
|
|
490
|
+
|
|
491
|
+
method_name = f"_visit_{type(node).__name__}"
|
|
492
|
+
# Convert CamelCase to snake_case for method lookup
|
|
493
|
+
visitor = getattr(self, method_name, None)
|
|
494
|
+
if visitor is not None:
|
|
495
|
+
return visitor(node)
|
|
496
|
+
|
|
497
|
+
# Fallback: try generic visit
|
|
498
|
+
return PineType.VOID
|
|
499
|
+
|
|
500
|
+
# ------------------------------------------------------------------
|
|
501
|
+
# Top-level visitors
|
|
502
|
+
# ------------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
def _visit_Program(self, node: Program) -> PineType:
|
|
505
|
+
for stmt in node.body:
|
|
506
|
+
prev_top = self._current_top_level_stmt
|
|
507
|
+
self._current_top_level_stmt = stmt
|
|
508
|
+
self._visit(stmt)
|
|
509
|
+
self._current_top_level_stmt = prev_top
|
|
510
|
+
return PineType.VOID
|
|
511
|
+
|
|
512
|
+
def _visit_StrategyDecl(self, node: StrategyDecl) -> PineType:
|
|
513
|
+
# Extract strategy parameters
|
|
514
|
+
if node.args:
|
|
515
|
+
# First arg is title
|
|
516
|
+
title_node = node.args[0]
|
|
517
|
+
if isinstance(title_node, StringLiteral):
|
|
518
|
+
self._strategy_params["title"] = title_node.value
|
|
519
|
+
for key, val_node in node.kwargs.items():
|
|
520
|
+
self._strategy_params[key] = self._extract_literal_value(val_node)
|
|
521
|
+
return PineType.VOID
|
|
522
|
+
|
|
523
|
+
def _visit_ImportStmt(self, node: ImportStmt) -> PineType:
|
|
524
|
+
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
525
|
+
diag = Diagnostic(
|
|
526
|
+
level=Level.ERROR,
|
|
527
|
+
phase=Phase.ANALYZER,
|
|
528
|
+
location=loc,
|
|
529
|
+
message=f"Import is not supported: '{node.path}'",
|
|
530
|
+
)
|
|
531
|
+
raise CompileError([diag])
|
|
532
|
+
|
|
533
|
+
# ------------------------------------------------------------------
|
|
534
|
+
# Declaration / assignment visitors
|
|
535
|
+
# ------------------------------------------------------------------
|
|
536
|
+
|
|
537
|
+
def _udt_name_from_ctor(self, value: ASTNode) -> str | None:
|
|
538
|
+
"""If value is ``TypeName.new(...)`` for a user-defined type, return TypeName."""
|
|
539
|
+
if not isinstance(value, FuncCall):
|
|
540
|
+
return None
|
|
541
|
+
cal = value.callee
|
|
542
|
+
if not isinstance(cal, MemberAccess) or not isinstance(cal.object, Identifier):
|
|
543
|
+
return None
|
|
544
|
+
owner = cal.object.name
|
|
545
|
+
if owner not in self._udt_fields:
|
|
546
|
+
return None
|
|
547
|
+
m = cal.member
|
|
548
|
+
if m == "new" or (isinstance(m, str) and m.startswith("new")):
|
|
549
|
+
return owner
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
def _visit_VarDecl(self, node: VarDecl) -> PineType:
|
|
553
|
+
# Infer type from the value expression
|
|
554
|
+
val_type = self._visit(node.value)
|
|
555
|
+
type_spec = self._type_spec_from_hint(node.type_hint) if node.type_hint else None
|
|
556
|
+
if type_spec is None:
|
|
557
|
+
type_spec = self._type_spec_from_expr(node.value)
|
|
558
|
+
|
|
559
|
+
# Check for type hint override
|
|
560
|
+
if node.type_hint:
|
|
561
|
+
hint_type = self._type_hint_to_pine(node.type_hint)
|
|
562
|
+
if hint_type != PineType.UNKNOWN:
|
|
563
|
+
val_type = hint_type
|
|
564
|
+
|
|
565
|
+
# Check for input calls
|
|
566
|
+
is_const = False
|
|
567
|
+
const_value = None
|
|
568
|
+
enum_type_name: str | None = None
|
|
569
|
+
if isinstance(node.value, FuncCall):
|
|
570
|
+
ic = self._check_input_call(node.value)
|
|
571
|
+
if ic is not None:
|
|
572
|
+
val_type, is_const, const_value = ic
|
|
573
|
+
enum_type_name = self._input_enum_type_name(node.value)
|
|
574
|
+
|
|
575
|
+
udt_ctor = self._udt_name_from_ctor(node.value)
|
|
576
|
+
# User function return propagation: if the value is a call to a user
|
|
577
|
+
# function whose body returns ``T.new(...)``, the local picks up
|
|
578
|
+
# type ``T``. Without this the caller's symbol would track as
|
|
579
|
+
# ``double``, breaking ``s.score()`` dispatch downstream. Probe:
|
|
580
|
+
# data/validation/udt-method-probe-20-udt-return-from-func.
|
|
581
|
+
if udt_ctor is None and isinstance(node.value, FuncCall):
|
|
582
|
+
cal = node.value.callee
|
|
583
|
+
if isinstance(cal, Identifier):
|
|
584
|
+
udt_ctor = self._func_udt_return_types.get(cal.name)
|
|
585
|
+
if udt_ctor is not None and type_spec is None:
|
|
586
|
+
type_spec = TypeSpec.udt(udt_ctor)
|
|
587
|
+
|
|
588
|
+
if self._global_scope:
|
|
589
|
+
if self._is_static_expression(node.value):
|
|
590
|
+
self._static_vars.add(node.name)
|
|
591
|
+
else:
|
|
592
|
+
self._static_vars.discard(node.name)
|
|
593
|
+
else:
|
|
594
|
+
self._static_vars.discard(node.name)
|
|
595
|
+
|
|
596
|
+
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
597
|
+
sym = Symbol(
|
|
598
|
+
name=node.name,
|
|
599
|
+
pine_type=val_type,
|
|
600
|
+
is_series=False,
|
|
601
|
+
is_var=node.is_var or node.is_varip,
|
|
602
|
+
is_const=is_const,
|
|
603
|
+
const_value=const_value,
|
|
604
|
+
scope=self._symbols.current_scope.name,
|
|
605
|
+
loc=loc,
|
|
606
|
+
enum_type_name=enum_type_name,
|
|
607
|
+
udt_type_name=udt_ctor,
|
|
608
|
+
type_spec=type_spec,
|
|
609
|
+
)
|
|
610
|
+
if node.name in self._static_vars:
|
|
611
|
+
setattr(sym, "is_static_series", True)
|
|
612
|
+
self._symbols.define(sym)
|
|
613
|
+
if udt_ctor is not None:
|
|
614
|
+
self._udt_var_types[node.name] = udt_ctor
|
|
615
|
+
if type_spec is not None:
|
|
616
|
+
self._collection_types[node.name] = type_spec
|
|
617
|
+
if type_spec.kind == "udt" and type_spec.name:
|
|
618
|
+
self._udt_var_types[node.name] = type_spec.name
|
|
619
|
+
|
|
620
|
+
# Track var members
|
|
621
|
+
if node.is_var or node.is_varip:
|
|
622
|
+
init_str = self._expr_to_str(node.value)
|
|
623
|
+
self._var_members.append((node.name, val_type, init_str))
|
|
624
|
+
# Capture the init AST too so codegen can inspect the RHS callee
|
|
625
|
+
# (used to detect int64-returning builtins like ``time()`` and
|
|
626
|
+
# promote the symbol storage type to ``int64_t``).
|
|
627
|
+
if node.value is not None:
|
|
628
|
+
self._var_member_init_exprs[node.name] = node.value
|
|
629
|
+
# Track function-scoped var members
|
|
630
|
+
scope_name = self._symbols.current_scope.name
|
|
631
|
+
if scope_name.startswith("func_"):
|
|
632
|
+
func_name = scope_name[5:] # strip "func_" prefix
|
|
633
|
+
if func_name not in self._func_var_members:
|
|
634
|
+
self._func_var_members[func_name] = []
|
|
635
|
+
self._func_var_members[func_name].append((node.name, val_type, init_str))
|
|
636
|
+
|
|
637
|
+
# Track global-scope non-var declarations (needed as class members
|
|
638
|
+
# so user functions can reference them)
|
|
639
|
+
if (not node.is_var and not node.is_varip
|
|
640
|
+
and self._symbols.current_scope.name == "global"
|
|
641
|
+
and node.name not in self._series_vars):
|
|
642
|
+
self._global_var_decls.append((node.name, val_type))
|
|
643
|
+
self._global_expr_map[node.name] = node.value
|
|
644
|
+
|
|
645
|
+
if self._symbols.current_scope.name == "global":
|
|
646
|
+
self._record_global_binding_stmt(
|
|
647
|
+
node.name,
|
|
648
|
+
val_type,
|
|
649
|
+
node.is_var or node.is_varip,
|
|
650
|
+
decl_node=node,
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
return val_type
|
|
654
|
+
|
|
655
|
+
def _visit_Assignment(self, node: Assignment) -> PineType:
|
|
656
|
+
# Visit the value first
|
|
657
|
+
val_type = self._visit(node.value)
|
|
658
|
+
|
|
659
|
+
udt_ctor = self._udt_name_from_ctor(node.value)
|
|
660
|
+
if isinstance(node.target, Identifier) and udt_ctor is not None:
|
|
661
|
+
self._udt_var_types[node.target.name] = udt_ctor
|
|
662
|
+
if isinstance(node.target, Identifier):
|
|
663
|
+
spec = self._type_spec_from_expr(node.value)
|
|
664
|
+
if spec is not None:
|
|
665
|
+
self._collection_types[node.target.name] = spec
|
|
666
|
+
|
|
667
|
+
# Resolve the target
|
|
668
|
+
if isinstance(node.target, Identifier):
|
|
669
|
+
sym = self._symbols.resolve(node.target.name)
|
|
670
|
+
if sym is None:
|
|
671
|
+
self._error(
|
|
672
|
+
f"Undefined variable: '{node.target.name}'",
|
|
673
|
+
node.loc or node.target.loc,
|
|
674
|
+
)
|
|
675
|
+
else:
|
|
676
|
+
if sym.scope == "global":
|
|
677
|
+
self._global_reassigned_names.add(node.target.name)
|
|
678
|
+
self._record_global_binding_stmt(
|
|
679
|
+
node.target.name,
|
|
680
|
+
sym.pine_type,
|
|
681
|
+
sym.is_var,
|
|
682
|
+
)
|
|
683
|
+
if self._global_scope:
|
|
684
|
+
if self._is_static_expression(node.value):
|
|
685
|
+
self._static_vars.add(node.target.name)
|
|
686
|
+
setattr(sym, "is_static_series", True)
|
|
687
|
+
else:
|
|
688
|
+
self._static_vars.discard(node.target.name)
|
|
689
|
+
if hasattr(sym, "is_static_series"):
|
|
690
|
+
delattr(sym, "is_static_series")
|
|
691
|
+
else:
|
|
692
|
+
self._static_vars.discard(node.target.name)
|
|
693
|
+
if hasattr(sym, "is_static_series"):
|
|
694
|
+
delattr(sym, "is_static_series")
|
|
695
|
+
else:
|
|
696
|
+
self._visit(node.target)
|
|
697
|
+
base_name = self._get_target_base_name(node.target)
|
|
698
|
+
if base_name:
|
|
699
|
+
self._static_vars.discard(base_name)
|
|
700
|
+
sym = self._symbols.resolve(base_name)
|
|
701
|
+
if sym and hasattr(sym, "is_static_series"):
|
|
702
|
+
delattr(sym, "is_static_series")
|
|
703
|
+
|
|
704
|
+
return val_type
|
|
705
|
+
|
|
706
|
+
def _visit_TupleAssign(self, node: TupleAssign) -> PineType:
|
|
707
|
+
val_type = self._visit(node.value)
|
|
708
|
+
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
709
|
+
|
|
710
|
+
is_val_static = self._is_static_expression(node.value)
|
|
711
|
+
|
|
712
|
+
for name in node.names:
|
|
713
|
+
if name == "_":
|
|
714
|
+
continue
|
|
715
|
+
|
|
716
|
+
if self._global_scope and is_val_static:
|
|
717
|
+
self._static_vars.add(name)
|
|
718
|
+
else:
|
|
719
|
+
self._static_vars.discard(name)
|
|
720
|
+
|
|
721
|
+
sym = Symbol(
|
|
722
|
+
name=name,
|
|
723
|
+
pine_type=PineType.FLOAT, # tuple elements are typically float
|
|
724
|
+
is_series=False,
|
|
725
|
+
is_var=False,
|
|
726
|
+
is_const=False,
|
|
727
|
+
const_value=None,
|
|
728
|
+
scope=self._symbols.current_scope.name,
|
|
729
|
+
loc=loc,
|
|
730
|
+
)
|
|
731
|
+
if name in self._static_vars:
|
|
732
|
+
setattr(sym, "is_static_series", True)
|
|
733
|
+
self._symbols.define(sym)
|
|
734
|
+
|
|
735
|
+
return val_type
|
|
736
|
+
|
|
737
|
+
# ------------------------------------------------------------------
|
|
738
|
+
# Function definition
|
|
739
|
+
# ------------------------------------------------------------------
|
|
740
|
+
|
|
741
|
+
def _visit_FuncDef(self, node: FuncDef) -> PineType:
|
|
742
|
+
# Store the function def for later analysis
|
|
743
|
+
self._func_defs[node.name] = node
|
|
744
|
+
|
|
745
|
+
# Enter function scope
|
|
746
|
+
self._symbols.enter_scope(f"func_{node.name}")
|
|
747
|
+
|
|
748
|
+
# Define parameters (type unknown until called)
|
|
749
|
+
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
750
|
+
for param in node.params:
|
|
751
|
+
sym = Symbol(
|
|
752
|
+
name=param,
|
|
753
|
+
pine_type=PineType.UNKNOWN,
|
|
754
|
+
is_series=False,
|
|
755
|
+
is_var=False,
|
|
756
|
+
is_const=False,
|
|
757
|
+
const_value=None,
|
|
758
|
+
scope=f"func_{node.name}",
|
|
759
|
+
loc=loc,
|
|
760
|
+
)
|
|
761
|
+
self._symbols.define(sym)
|
|
762
|
+
|
|
763
|
+
# Record TA counter before visiting body
|
|
764
|
+
ta_start = len(self._ta_call_sites)
|
|
765
|
+
|
|
766
|
+
# Visit body to discover return type
|
|
767
|
+
body_type = PineType.VOID
|
|
768
|
+
old_global = self._global_scope
|
|
769
|
+
self._global_scope = False
|
|
770
|
+
try:
|
|
771
|
+
for stmt in node.body:
|
|
772
|
+
body_type = self._visit(stmt)
|
|
773
|
+
finally:
|
|
774
|
+
self._global_scope = old_global
|
|
775
|
+
|
|
776
|
+
# Record TA range for this function
|
|
777
|
+
ta_end = len(self._ta_call_sites)
|
|
778
|
+
if ta_end > ta_start:
|
|
779
|
+
self._func_ta_ranges[node.name] = (ta_start, ta_end)
|
|
780
|
+
|
|
781
|
+
self._symbols.exit_scope()
|
|
782
|
+
|
|
783
|
+
# Detect if function returns a tuple (last stmt is TupleLiteral)
|
|
784
|
+
self._func_returns_tuple[node.name] = False
|
|
785
|
+
self._func_tuple_element_count[node.name] = 0
|
|
786
|
+
if node.body:
|
|
787
|
+
last_stmt = node.body[-1]
|
|
788
|
+
tuple_node = None
|
|
789
|
+
if isinstance(last_stmt, ExprStmt) and isinstance(last_stmt.expr, TupleLiteral):
|
|
790
|
+
tuple_node = last_stmt.expr
|
|
791
|
+
elif isinstance(last_stmt, TupleLiteral):
|
|
792
|
+
tuple_node = last_stmt
|
|
793
|
+
if tuple_node is not None:
|
|
794
|
+
self._func_returns_tuple[node.name] = True
|
|
795
|
+
self._func_tuple_element_count[node.name] = len(tuple_node.elements)
|
|
796
|
+
|
|
797
|
+
# Detect if the function returns a UDT instance via ``T.new(...)`` —
|
|
798
|
+
# used by codegen to emit the C++ return type as the struct name and
|
|
799
|
+
# to propagate UDT typing onto the caller's local. Probe:
|
|
800
|
+
# data/validation/udt-method-probe-20-udt-return-from-func.
|
|
801
|
+
if node.body:
|
|
802
|
+
last_stmt = node.body[-1]
|
|
803
|
+
ret_expr = None
|
|
804
|
+
if isinstance(last_stmt, ExprStmt):
|
|
805
|
+
ret_expr = last_stmt.expr
|
|
806
|
+
elif not isinstance(last_stmt, (TupleLiteral,)):
|
|
807
|
+
# last_stmt is itself an expression node (single-expr funcs)
|
|
808
|
+
ret_expr = last_stmt if hasattr(last_stmt, "loc") else None
|
|
809
|
+
udt_ret = self._udt_name_from_ctor(ret_expr) if ret_expr is not None else None
|
|
810
|
+
if udt_ret is not None:
|
|
811
|
+
self._func_udt_return_types[node.name] = udt_ret
|
|
812
|
+
|
|
813
|
+
# Store return type
|
|
814
|
+
self._func_return_types[node.name] = body_type
|
|
815
|
+
|
|
816
|
+
# Define the function name in the enclosing scope
|
|
817
|
+
sym = Symbol(
|
|
818
|
+
name=node.name,
|
|
819
|
+
pine_type=body_type,
|
|
820
|
+
is_series=False,
|
|
821
|
+
is_var=False,
|
|
822
|
+
is_const=False,
|
|
823
|
+
const_value=None,
|
|
824
|
+
scope=self._symbols.current_scope.name,
|
|
825
|
+
loc=loc,
|
|
826
|
+
)
|
|
827
|
+
self._symbols.define(sym)
|
|
828
|
+
|
|
829
|
+
return PineType.VOID
|
|
830
|
+
|
|
831
|
+
# ------------------------------------------------------------------
|
|
832
|
+
# UDT visitors
|
|
833
|
+
# ------------------------------------------------------------------
|
|
834
|
+
|
|
835
|
+
def _visit_TypeDecl(self, node) -> PineType:
|
|
836
|
+
"""Register UDT name and field types in symbol table."""
|
|
837
|
+
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
838
|
+
self._symbols.define(Symbol(
|
|
839
|
+
name=node.name, pine_type=PineType.FLOAT, is_series=False,
|
|
840
|
+
is_var=False, is_const=True, const_value=None,
|
|
841
|
+
scope="global", loc=loc,
|
|
842
|
+
))
|
|
843
|
+
self._udt_fields[node.name] = {
|
|
844
|
+
f.name: self._type_hint_to_pine(f.type_name) for f in node.fields
|
|
845
|
+
}
|
|
846
|
+
self._udt_field_type_specs[node.name] = {
|
|
847
|
+
f.name: self._type_spec_from_hint(f.type_name) for f in node.fields
|
|
848
|
+
if self._type_spec_from_hint(f.type_name) is not None
|
|
849
|
+
}
|
|
850
|
+
for f in node.fields:
|
|
851
|
+
if f.default:
|
|
852
|
+
self._visit(f.default)
|
|
853
|
+
return PineType.VOID
|
|
854
|
+
|
|
855
|
+
def _visit_EnumDecl(self, node) -> PineType:
|
|
856
|
+
"""Register user enum (derived type): ordinals in _enum_defs, field strings separately."""
|
|
857
|
+
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
858
|
+
# Enum *type* name in scope uses INT as a placeholder; real typing is via enum_defs.
|
|
859
|
+
self._symbols.define(Symbol(
|
|
860
|
+
name=node.name, pine_type=PineType.INT, is_series=False,
|
|
861
|
+
is_var=False, is_const=True, const_value=None,
|
|
862
|
+
scope="global", loc=loc,
|
|
863
|
+
))
|
|
864
|
+
self._enum_defs[node.name] = node.members
|
|
865
|
+
# Parallel string payloads (arbitrary per field in TV); str.tostring uses this, not ordinals
|
|
866
|
+
strs: list[str] = []
|
|
867
|
+
for m in node.members:
|
|
868
|
+
av = node.member_values.get(m)
|
|
869
|
+
if isinstance(av, StringLiteral):
|
|
870
|
+
strs.append(av.value)
|
|
871
|
+
else:
|
|
872
|
+
strs.append(m)
|
|
873
|
+
self._enum_member_strings[node.name] = strs
|
|
874
|
+
return PineType.VOID
|
|
875
|
+
|
|
876
|
+
def _visit_MethodDef(self, node) -> PineType:
|
|
877
|
+
"""Register UDT instance method under a unique key ``TypeName.methodName``."""
|
|
878
|
+
method_key = f"{node.type_name}.{node.name}"
|
|
879
|
+
self._symbols.enter_scope(f"method_{node.type_name}_{node.name}")
|
|
880
|
+
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
881
|
+
param_hints = (node.annotations or {}).get("param_type_hints", [])
|
|
882
|
+
param_types: list[PineType] = []
|
|
883
|
+
for i, p in enumerate(node.params):
|
|
884
|
+
udt_self = node.type_name if i == 0 else None
|
|
885
|
+
hint = param_hints[i] if i < len(param_hints) else None
|
|
886
|
+
ptype = self._type_hint_to_pine(hint) if hint else PineType.FLOAT
|
|
887
|
+
pspec = self._type_spec_from_hint(hint) if hint else None
|
|
888
|
+
param_types.append(ptype)
|
|
889
|
+
self._symbols.define(Symbol(
|
|
890
|
+
name=p, pine_type=ptype, is_series=False,
|
|
891
|
+
is_var=False, is_const=False, const_value=None,
|
|
892
|
+
scope=self._symbols.current_scope.name, loc=loc,
|
|
893
|
+
udt_type_name=udt_self,
|
|
894
|
+
type_spec=pspec,
|
|
895
|
+
))
|
|
896
|
+
ret_type = PineType.VOID
|
|
897
|
+
old_global = self._global_scope
|
|
898
|
+
self._global_scope = False
|
|
899
|
+
try:
|
|
900
|
+
for stmt in node.body:
|
|
901
|
+
ret_type = self._visit(stmt)
|
|
902
|
+
finally:
|
|
903
|
+
self._global_scope = old_global
|
|
904
|
+
self._symbols.exit_scope()
|
|
905
|
+
|
|
906
|
+
# Detect tuple return on UDT methods (mirrors the regular FuncDef logic
|
|
907
|
+
# earlier in this file). Without this, codegen emits the method with a
|
|
908
|
+
# scalar return type and clang chokes on the ``std::make_tuple(...)``
|
|
909
|
+
# body. Probe: data/validation/udt-method-probe-17-tuple-return-destructure.
|
|
910
|
+
returns_tuple = False
|
|
911
|
+
tuple_element_count = 0
|
|
912
|
+
if node.body:
|
|
913
|
+
last_stmt = node.body[-1]
|
|
914
|
+
tuple_node = None
|
|
915
|
+
if isinstance(last_stmt, ExprStmt) and isinstance(last_stmt.expr, TupleLiteral):
|
|
916
|
+
tuple_node = last_stmt.expr
|
|
917
|
+
elif isinstance(last_stmt, TupleLiteral):
|
|
918
|
+
tuple_node = last_stmt
|
|
919
|
+
if tuple_node is not None:
|
|
920
|
+
returns_tuple = True
|
|
921
|
+
tuple_element_count = len(tuple_node.elements)
|
|
922
|
+
|
|
923
|
+
# Forward the parser-captured per-param defaults onto the FuncInfo so
|
|
924
|
+
# codegen can fill in missing args at UDT-method call sites. Probe:
|
|
925
|
+
# data/validation/udt-method-probe-04-default-param.
|
|
926
|
+
param_defaults = list((node.annotations or {}).get("param_defaults", []))
|
|
927
|
+
# Pad to len(params) for safety when an older parser did not record
|
|
928
|
+
# them (e.g., synthetic MethodDef nodes from tests).
|
|
929
|
+
while len(param_defaults) < len(node.params):
|
|
930
|
+
param_defaults.append(None)
|
|
931
|
+
fi = FuncInfo(
|
|
932
|
+
name=method_key,
|
|
933
|
+
param_types=param_types,
|
|
934
|
+
return_type=ret_type,
|
|
935
|
+
node=FuncDef(name=node.name, params=node.params,
|
|
936
|
+
body=node.body, is_single_expr=node.is_single_expr),
|
|
937
|
+
is_udt_method=True,
|
|
938
|
+
udt_type_name=node.type_name,
|
|
939
|
+
returns_tuple=returns_tuple,
|
|
940
|
+
tuple_element_count=tuple_element_count,
|
|
941
|
+
param_defaults=param_defaults,
|
|
942
|
+
)
|
|
943
|
+
self._func_infos.append(fi)
|
|
944
|
+
return PineType.VOID
|
|
945
|
+
|
|
946
|
+
# ------------------------------------------------------------------
|
|
947
|
+
# Control flow
|
|
948
|
+
# ------------------------------------------------------------------
|
|
949
|
+
|
|
950
|
+
def _visit_IfStmt(self, node: IfStmt) -> PineType:
|
|
951
|
+
old_global = self._global_scope
|
|
952
|
+
self._global_scope = False
|
|
953
|
+
try:
|
|
954
|
+
self._visit(node.condition)
|
|
955
|
+
body_type = PineType.VOID
|
|
956
|
+
for stmt in node.body:
|
|
957
|
+
body_type = self._visit(stmt)
|
|
958
|
+
for stmt in node.else_body:
|
|
959
|
+
self._visit(stmt)
|
|
960
|
+
finally:
|
|
961
|
+
self._global_scope = old_global
|
|
962
|
+
# If used as expression (x = if ...), return last expr type
|
|
963
|
+
return body_type
|
|
964
|
+
|
|
965
|
+
def _visit_ForStmt(self, node: ForStmt) -> PineType:
|
|
966
|
+
self._symbols.enter_scope("for")
|
|
967
|
+
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
968
|
+
|
|
969
|
+
# Define loop variable
|
|
970
|
+
sym = Symbol(
|
|
971
|
+
name=node.var,
|
|
972
|
+
pine_type=PineType.INT,
|
|
973
|
+
is_series=False,
|
|
974
|
+
is_var=False,
|
|
975
|
+
is_const=False,
|
|
976
|
+
const_value=None,
|
|
977
|
+
scope="for",
|
|
978
|
+
loc=loc,
|
|
979
|
+
)
|
|
980
|
+
self._symbols.define(sym)
|
|
981
|
+
|
|
982
|
+
old_global = self._global_scope
|
|
983
|
+
self._global_scope = False
|
|
984
|
+
try:
|
|
985
|
+
self._visit(node.start)
|
|
986
|
+
self._visit(node.end)
|
|
987
|
+
if node.step:
|
|
988
|
+
self._visit(node.step)
|
|
989
|
+
for stmt in node.body:
|
|
990
|
+
self._visit(stmt)
|
|
991
|
+
finally:
|
|
992
|
+
self._global_scope = old_global
|
|
993
|
+
|
|
994
|
+
self._symbols.exit_scope()
|
|
995
|
+
return PineType.VOID
|
|
996
|
+
|
|
997
|
+
def _visit_ForInStmt(self, node) -> PineType:
|
|
998
|
+
old_global = self._global_scope
|
|
999
|
+
self._global_scope = False
|
|
1000
|
+
try:
|
|
1001
|
+
self._visit(node.iterable)
|
|
1002
|
+
self._symbols.enter_scope("for_in")
|
|
1003
|
+
if node.var:
|
|
1004
|
+
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
1005
|
+
self._symbols.define(Symbol(
|
|
1006
|
+
name=node.var, pine_type=PineType.FLOAT, is_series=False,
|
|
1007
|
+
is_var=False, is_const=False, const_value=None,
|
|
1008
|
+
scope=self._symbols.current_scope.name, loc=loc,
|
|
1009
|
+
))
|
|
1010
|
+
if node.vars:
|
|
1011
|
+
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
1012
|
+
for v in node.vars:
|
|
1013
|
+
self._symbols.define(Symbol(
|
|
1014
|
+
name=v, pine_type=PineType.FLOAT, is_series=False,
|
|
1015
|
+
is_var=False, is_const=False, const_value=None,
|
|
1016
|
+
scope=self._symbols.current_scope.name, loc=loc,
|
|
1017
|
+
))
|
|
1018
|
+
for stmt in node.body:
|
|
1019
|
+
self._visit(stmt)
|
|
1020
|
+
self._symbols.exit_scope()
|
|
1021
|
+
finally:
|
|
1022
|
+
self._global_scope = old_global
|
|
1023
|
+
return PineType.VOID
|
|
1024
|
+
|
|
1025
|
+
def _visit_WhileStmt(self, node: WhileStmt) -> PineType:
|
|
1026
|
+
old_global = self._global_scope
|
|
1027
|
+
self._global_scope = False
|
|
1028
|
+
try:
|
|
1029
|
+
self._visit(node.condition)
|
|
1030
|
+
for stmt in node.body:
|
|
1031
|
+
self._visit(stmt)
|
|
1032
|
+
finally:
|
|
1033
|
+
self._global_scope = old_global
|
|
1034
|
+
return PineType.VOID
|
|
1035
|
+
|
|
1036
|
+
def _visit_SwitchStmt(self, node: SwitchStmt) -> PineType:
|
|
1037
|
+
old_global = self._global_scope
|
|
1038
|
+
self._global_scope = False
|
|
1039
|
+
try:
|
|
1040
|
+
if node.expr:
|
|
1041
|
+
self._visit(node.expr)
|
|
1042
|
+
result_type = PineType.VOID
|
|
1043
|
+
for case_expr, case_body in node.cases:
|
|
1044
|
+
if case_expr:
|
|
1045
|
+
self._visit(case_expr)
|
|
1046
|
+
for stmt in case_body:
|
|
1047
|
+
result_type = self._visit(stmt)
|
|
1048
|
+
for stmt in node.default_body:
|
|
1049
|
+
self._visit(stmt)
|
|
1050
|
+
finally:
|
|
1051
|
+
self._global_scope = old_global
|
|
1052
|
+
# If used as expression (x = switch ...), return last expr type
|
|
1053
|
+
return result_type
|
|
1054
|
+
|
|
1055
|
+
def _visit_BreakStmt(self, node: BreakStmt) -> PineType:
|
|
1056
|
+
return PineType.VOID
|
|
1057
|
+
|
|
1058
|
+
def _visit_ContinueStmt(self, node: ContinueStmt) -> PineType:
|
|
1059
|
+
return PineType.VOID
|
|
1060
|
+
|
|
1061
|
+
# ------------------------------------------------------------------
|
|
1062
|
+
# Expression wrapper
|
|
1063
|
+
# ------------------------------------------------------------------
|
|
1064
|
+
|
|
1065
|
+
def _visit_ExprStmt(self, node: ExprStmt) -> PineType:
|
|
1066
|
+
return self._visit(node.expr)
|
|
1067
|
+
|
|
1068
|
+
# ------------------------------------------------------------------
|
|
1069
|
+
# Expression visitors
|
|
1070
|
+
# ------------------------------------------------------------------
|
|
1071
|
+
|
|
1072
|
+
def _visit_BinOp(self, node: BinOp) -> PineType:
|
|
1073
|
+
left_type = self._visit(node.left)
|
|
1074
|
+
right_type = self._visit(node.right)
|
|
1075
|
+
|
|
1076
|
+
# Comparison and logical operators return BOOL
|
|
1077
|
+
if node.op in ("==", "!=", ">", "<", ">=", "<=", "and", "or"):
|
|
1078
|
+
return PineType.BOOL
|
|
1079
|
+
|
|
1080
|
+
# String concatenation: if either side is STRING, result is STRING
|
|
1081
|
+
if left_type == PineType.STRING or right_type == PineType.STRING:
|
|
1082
|
+
return PineType.STRING
|
|
1083
|
+
|
|
1084
|
+
# Arithmetic: promote to FLOAT if either side is FLOAT
|
|
1085
|
+
if left_type == PineType.FLOAT or right_type == PineType.FLOAT:
|
|
1086
|
+
return PineType.FLOAT
|
|
1087
|
+
if left_type == PineType.INT and right_type == PineType.INT:
|
|
1088
|
+
return PineType.INT
|
|
1089
|
+
|
|
1090
|
+
# Division always returns FLOAT
|
|
1091
|
+
if node.op == "/":
|
|
1092
|
+
return PineType.FLOAT
|
|
1093
|
+
|
|
1094
|
+
return PineType.FLOAT # default
|
|
1095
|
+
|
|
1096
|
+
def _visit_UnaryOp(self, node: UnaryOp) -> PineType:
|
|
1097
|
+
operand_type = self._visit(node.operand)
|
|
1098
|
+
if node.op == "not":
|
|
1099
|
+
return PineType.BOOL
|
|
1100
|
+
return operand_type
|
|
1101
|
+
|
|
1102
|
+
def _visit_Ternary(self, node: Ternary) -> PineType:
|
|
1103
|
+
self._visit(node.condition)
|
|
1104
|
+
true_type = self._visit(node.true_val)
|
|
1105
|
+
false_type = self._visit(node.false_val)
|
|
1106
|
+
|
|
1107
|
+
# String type dominates
|
|
1108
|
+
if true_type == PineType.STRING or false_type == PineType.STRING:
|
|
1109
|
+
return PineType.STRING
|
|
1110
|
+
# Promote types
|
|
1111
|
+
if true_type == PineType.FLOAT or false_type == PineType.FLOAT:
|
|
1112
|
+
return PineType.FLOAT
|
|
1113
|
+
return true_type
|
|
1114
|
+
|
|
1115
|
+
def _visit_FuncCall(self, node: FuncCall) -> PineType:
|
|
1116
|
+
# Determine what is being called
|
|
1117
|
+
callee = node.callee
|
|
1118
|
+
|
|
1119
|
+
if isinstance(callee, MemberAccess):
|
|
1120
|
+
obj = callee.object
|
|
1121
|
+
member = callee.member
|
|
1122
|
+
|
|
1123
|
+
# ta.* calls
|
|
1124
|
+
if isinstance(obj, Identifier) and obj.name == "ta":
|
|
1125
|
+
if member == "sum":
|
|
1126
|
+
self._error(
|
|
1127
|
+
"PineScript has no ta.sum; use math.sum(source, length) for rolling sum",
|
|
1128
|
+
node.loc,
|
|
1129
|
+
)
|
|
1130
|
+
return self._handle_ta_call(member, node)
|
|
1131
|
+
|
|
1132
|
+
# strategy.* calls
|
|
1133
|
+
if isinstance(obj, Identifier) and obj.name == "strategy":
|
|
1134
|
+
return self._handle_strategy_call(member, node)
|
|
1135
|
+
|
|
1136
|
+
# input.* calls
|
|
1137
|
+
if isinstance(obj, Identifier) and obj.name == "input":
|
|
1138
|
+
return self._handle_input_member_call(member, node)
|
|
1139
|
+
|
|
1140
|
+
# math.* calls
|
|
1141
|
+
if isinstance(obj, Identifier) and obj.name == "math":
|
|
1142
|
+
# math.sum is a rolling sum — redirect to TA handling
|
|
1143
|
+
if member == "sum":
|
|
1144
|
+
return self._handle_ta_call("sum", node)
|
|
1145
|
+
# Visit args
|
|
1146
|
+
for arg in node.args:
|
|
1147
|
+
self._visit(arg)
|
|
1148
|
+
return PineType.FLOAT
|
|
1149
|
+
|
|
1150
|
+
# str.* calls
|
|
1151
|
+
if isinstance(obj, Identifier) and obj.name == "str":
|
|
1152
|
+
for arg in node.args:
|
|
1153
|
+
self._visit(arg)
|
|
1154
|
+
return PineType.STRING
|
|
1155
|
+
|
|
1156
|
+
# request.* calls
|
|
1157
|
+
if isinstance(obj, Identifier) and obj.name == "request":
|
|
1158
|
+
return self._handle_request_call(member, node)
|
|
1159
|
+
|
|
1160
|
+
# General member call (e.g., array.push, etc.)
|
|
1161
|
+
self._visit(obj)
|
|
1162
|
+
for arg in node.args:
|
|
1163
|
+
self._visit(arg)
|
|
1164
|
+
for val in node.kwargs.values():
|
|
1165
|
+
self._visit(val)
|
|
1166
|
+
# Matrix method dispatch: ``m.get(0, 0)`` on ``matrix<int>`` must
|
|
1167
|
+
# type as INT, not VOID, so ``v = m.get(...)`` propagates the
|
|
1168
|
+
# element PineType. ``_type_spec_from_expr`` already carries the
|
|
1169
|
+
# full TypeSpec for downstream codegen; this branch keeps the
|
|
1170
|
+
# legacy PineType-slot consumers (Symbol.pine_type, scalar
|
|
1171
|
+
# arithmetic inference) honest. See call_handlers.py
|
|
1172
|
+
# ``_handle_matrix_method``.
|
|
1173
|
+
if isinstance(obj, Identifier):
|
|
1174
|
+
recv_spec = self._collection_types.get(obj.name)
|
|
1175
|
+
if recv_spec is not None and recv_spec.kind == "matrix":
|
|
1176
|
+
return self._handle_matrix_method(member, recv_spec)
|
|
1177
|
+
return PineType.VOID
|
|
1178
|
+
|
|
1179
|
+
if isinstance(callee, Identifier):
|
|
1180
|
+
func_name = callee.name
|
|
1181
|
+
|
|
1182
|
+
# Skip functions (plot, etc.)
|
|
1183
|
+
if func_name in SKIP_FUNCS:
|
|
1184
|
+
# Still visit args for side effects
|
|
1185
|
+
for arg in node.args:
|
|
1186
|
+
self._visit(arg)
|
|
1187
|
+
for val in node.kwargs.values():
|
|
1188
|
+
self._visit(val)
|
|
1189
|
+
return PineType.VOID
|
|
1190
|
+
|
|
1191
|
+
# input() without qualifier
|
|
1192
|
+
if func_name == "input":
|
|
1193
|
+
return self._handle_input_call(node)
|
|
1194
|
+
|
|
1195
|
+
# fixnan
|
|
1196
|
+
if func_name == "fixnan":
|
|
1197
|
+
return self._handle_fixnan_call(node)
|
|
1198
|
+
|
|
1199
|
+
# nz
|
|
1200
|
+
if func_name == "nz":
|
|
1201
|
+
for arg in node.args:
|
|
1202
|
+
self._visit(arg)
|
|
1203
|
+
return PineType.FLOAT
|
|
1204
|
+
|
|
1205
|
+
# na() as function
|
|
1206
|
+
if func_name == "na":
|
|
1207
|
+
for arg in node.args:
|
|
1208
|
+
self._visit(arg)
|
|
1209
|
+
return PineType.BOOL
|
|
1210
|
+
|
|
1211
|
+
# color.* (e.g., color.new, color.rgb)
|
|
1212
|
+
if func_name == "color":
|
|
1213
|
+
for arg in node.args:
|
|
1214
|
+
self._visit(arg)
|
|
1215
|
+
return PineType.COLOR
|
|
1216
|
+
|
|
1217
|
+
# User-defined function call
|
|
1218
|
+
if func_name in self._func_defs:
|
|
1219
|
+
return self._handle_user_func_call(func_name, node)
|
|
1220
|
+
|
|
1221
|
+
# Built-in functions we don't specifically handle
|
|
1222
|
+
# Visit args for side effects
|
|
1223
|
+
for arg in node.args:
|
|
1224
|
+
self._visit(arg)
|
|
1225
|
+
for val in node.kwargs.values():
|
|
1226
|
+
self._visit(val)
|
|
1227
|
+
|
|
1228
|
+
# Check if it's a known symbol
|
|
1229
|
+
sym = self._symbols.resolve(func_name)
|
|
1230
|
+
if sym is not None:
|
|
1231
|
+
return sym.pine_type
|
|
1232
|
+
|
|
1233
|
+
return PineType.FLOAT # default for unknown functions
|
|
1234
|
+
|
|
1235
|
+
# Fallback
|
|
1236
|
+
self._visit(callee)
|
|
1237
|
+
for arg in node.args:
|
|
1238
|
+
self._visit(arg)
|
|
1239
|
+
for val in node.kwargs.values():
|
|
1240
|
+
self._visit(val)
|
|
1241
|
+
return PineType.VOID
|
|
1242
|
+
|
|
1243
|
+
def _visit_Subscript(self, node: Subscript) -> PineType:
|
|
1244
|
+
obj_type = self._visit(node.object)
|
|
1245
|
+
self._visit(node.index)
|
|
1246
|
+
|
|
1247
|
+
# Detect series vars / bar fields
|
|
1248
|
+
if isinstance(node.object, Identifier):
|
|
1249
|
+
name = node.object.name
|
|
1250
|
+
if name in BAR_FIELDS:
|
|
1251
|
+
self._series_bar_fields.add(name)
|
|
1252
|
+
else:
|
|
1253
|
+
sym = self._symbols.resolve(name)
|
|
1254
|
+
if sym is not None:
|
|
1255
|
+
if getattr(sym, "type_spec", None) is None or sym.type_spec.kind not in ("array", "map"):
|
|
1256
|
+
self._series_vars.add(name)
|
|
1257
|
+
sym.is_series = True
|
|
1258
|
+
# Track function-scoped series vars
|
|
1259
|
+
if sym.scope and sym.scope.startswith("func_"):
|
|
1260
|
+
func_name = sym.scope[5:]
|
|
1261
|
+
if func_name not in self._func_series_vars:
|
|
1262
|
+
self._func_series_vars[func_name] = set()
|
|
1263
|
+
self._func_series_vars[func_name].add(name)
|
|
1264
|
+
|
|
1265
|
+
return obj_type
|
|
1266
|
+
|
|
1267
|
+
def _visit_Identifier(self, node: Identifier) -> PineType:
|
|
1268
|
+
# Some identifiers are namespace prefixes handled elsewhere
|
|
1269
|
+
if node.name in ("strategy", "ta", "input", "math", "str", "color",
|
|
1270
|
+
"display", "syminfo", "timeframe", "plot",
|
|
1271
|
+
"alert", "barstate", "position", "shape", "location",
|
|
1272
|
+
"size", "currency", "order", "format", "text",
|
|
1273
|
+
"extend", "xloc", "yloc", "label", "line", "box",
|
|
1274
|
+
"table", "ticker", "request", "runtime", "chart",
|
|
1275
|
+
"barmerge", "adjustment", "earnings", "dividends",
|
|
1276
|
+
"splits", "session", "scale", "font",
|
|
1277
|
+
"hline", "backadjustment", "settlement_as_close",
|
|
1278
|
+
"dayofweek"):
|
|
1279
|
+
return PineType.VOID
|
|
1280
|
+
|
|
1281
|
+
sym = self._symbols.resolve(node.name)
|
|
1282
|
+
if sym is not None:
|
|
1283
|
+
return sym.pine_type
|
|
1284
|
+
|
|
1285
|
+
# Check if it's a user-defined function
|
|
1286
|
+
if node.name in self._func_defs:
|
|
1287
|
+
return self._func_return_types.get(node.name, PineType.FLOAT)
|
|
1288
|
+
|
|
1289
|
+
# Check for well-known PineScript built-in functions/types
|
|
1290
|
+
# that we didn't pre-populate (nz, na, fixnan, etc.)
|
|
1291
|
+
if node.name in ("nz", "na", "fixnan", "int", "float", "bool", "string",
|
|
1292
|
+
"array", "matrix", "label", "line", "box", "table",
|
|
1293
|
+
"log", "map", "type", "__array_literal__",
|
|
1294
|
+
"timestamp", "year", "month", "dayofmonth",
|
|
1295
|
+
"dayofweek", "hour", "minute", "second",
|
|
1296
|
+
"max_bars_back", "timenow", "barssince",
|
|
1297
|
+
"ta", "math", "str", "input", "color",
|
|
1298
|
+
"request", "ticker", "runtime"):
|
|
1299
|
+
return PineType.VOID
|
|
1300
|
+
|
|
1301
|
+
# Unknown identifier — treat as float (may be from skipped enum/type blocks)
|
|
1302
|
+
return PineType.FLOAT
|
|
1303
|
+
|
|
1304
|
+
def _visit_MemberAccess(self, node: MemberAccess) -> PineType:
|
|
1305
|
+
# Handle specific namespaces
|
|
1306
|
+
if isinstance(node.object, Identifier):
|
|
1307
|
+
ns = node.object.name
|
|
1308
|
+
|
|
1309
|
+
# strategy.* variables and constants
|
|
1310
|
+
if ns == "strategy":
|
|
1311
|
+
# Direction constants
|
|
1312
|
+
if node.member in ("long", "short"):
|
|
1313
|
+
return PineType.INT
|
|
1314
|
+
# Qty type constants
|
|
1315
|
+
if node.member in ("percent_of_equity", "fixed", "cash"):
|
|
1316
|
+
return PineType.INT
|
|
1317
|
+
# Commission type constants
|
|
1318
|
+
if node.member in ("commission",):
|
|
1319
|
+
return PineType.VOID # namespace prefix for .percent etc.
|
|
1320
|
+
# Integer strategy variables
|
|
1321
|
+
if node.member in ("closedtrades", "opentrades", "wintrades",
|
|
1322
|
+
"losstrades", "eventrades"):
|
|
1323
|
+
return PineType.INT
|
|
1324
|
+
# Float strategy variables
|
|
1325
|
+
if node.member in ("position_size", "position_avg_price",
|
|
1326
|
+
"equity", "initial_capital", "netprofit",
|
|
1327
|
+
"netprofit_percent", "openprofit",
|
|
1328
|
+
"openprofit_percent", "grossprofit",
|
|
1329
|
+
"grossprofit_percent", "grossloss",
|
|
1330
|
+
"grossloss_percent", "max_drawdown",
|
|
1331
|
+
"max_drawdown_percent", "max_runup",
|
|
1332
|
+
"max_runup_percent", "avg_trade",
|
|
1333
|
+
"avg_trade_percent", "avg_winning_trade",
|
|
1334
|
+
"avg_winning_trade_percent",
|
|
1335
|
+
"avg_losing_trade", "avg_losing_trade_percent",
|
|
1336
|
+
"margin_liquidation_price",
|
|
1337
|
+
"max_contracts_held_all",
|
|
1338
|
+
"max_contracts_held_long",
|
|
1339
|
+
"max_contracts_held_short"):
|
|
1340
|
+
return PineType.FLOAT
|
|
1341
|
+
# String strategy variables
|
|
1342
|
+
if node.member in ("account_currency", "position_entry_name"):
|
|
1343
|
+
return PineType.STRING
|
|
1344
|
+
# OCA / direction sub-namespaces
|
|
1345
|
+
if node.member in ("oca", "direction", "risk"):
|
|
1346
|
+
return PineType.VOID # namespace prefix
|
|
1347
|
+
# Default for unknown strategy members
|
|
1348
|
+
return PineType.INT
|
|
1349
|
+
|
|
1350
|
+
# ta.tr (no parens -- it's a property, not a function call)
|
|
1351
|
+
if ns == "ta":
|
|
1352
|
+
if node.member == "tr":
|
|
1353
|
+
# ta.tr uses close for previous bar
|
|
1354
|
+
self._series_bar_fields.add("close")
|
|
1355
|
+
self._series_bar_fields.add("high")
|
|
1356
|
+
self._series_bar_fields.add("low")
|
|
1357
|
+
return PineType.FLOAT
|
|
1358
|
+
# No-arg TA indicators used as properties (ta.obv, ta.accdist, etc.)
|
|
1359
|
+
_TA_PROPERTY_INDICATORS = {
|
|
1360
|
+
"obv", "accdist", "nvi", "pvi", "pvt", "wad", "wvad", "iii", "vwap",
|
|
1361
|
+
}
|
|
1362
|
+
if node.member in _TA_PROPERTY_INDICATORS and node.member in TA_CLASS_MAP:
|
|
1363
|
+
# Create a synthetic FuncCall node for the analyzer
|
|
1364
|
+
synthetic_call = FuncCall(
|
|
1365
|
+
callee=MemberAccess(object=Identifier(name="ta"), member=node.member),
|
|
1366
|
+
args=[], kwargs={},
|
|
1367
|
+
)
|
|
1368
|
+
return self._handle_ta_call(node.member, synthetic_call)
|
|
1369
|
+
return PineType.FLOAT
|
|
1370
|
+
|
|
1371
|
+
# math.* properties
|
|
1372
|
+
if ns == "math":
|
|
1373
|
+
return PineType.FLOAT
|
|
1374
|
+
|
|
1375
|
+
# syminfo.*
|
|
1376
|
+
if ns == "syminfo":
|
|
1377
|
+
if node.member == "mintick":
|
|
1378
|
+
return PineType.FLOAT
|
|
1379
|
+
return PineType.STRING
|
|
1380
|
+
|
|
1381
|
+
# color.* constants
|
|
1382
|
+
if ns == "color":
|
|
1383
|
+
return PineType.COLOR
|
|
1384
|
+
|
|
1385
|
+
# display.* constants
|
|
1386
|
+
if ns == "display":
|
|
1387
|
+
return PineType.INT
|
|
1388
|
+
|
|
1389
|
+
# plot.* constants (e.g., plot.style_areabr)
|
|
1390
|
+
if ns == "plot":
|
|
1391
|
+
return PineType.INT
|
|
1392
|
+
|
|
1393
|
+
# timeframe.*
|
|
1394
|
+
if ns == "timeframe":
|
|
1395
|
+
return PineType.STRING
|
|
1396
|
+
|
|
1397
|
+
# barstate.* (ishistory, isrealtime, islast, isfirst, etc.)
|
|
1398
|
+
if ns == "barstate":
|
|
1399
|
+
return PineType.BOOL
|
|
1400
|
+
|
|
1401
|
+
# alert.* constants (freq_once_per_bar, freq_once_per_bar_close, etc.)
|
|
1402
|
+
if ns == "alert":
|
|
1403
|
+
return PineType.INT
|
|
1404
|
+
|
|
1405
|
+
# position.* constants for tables (middle_right, top_left, etc.)
|
|
1406
|
+
if ns == "position":
|
|
1407
|
+
return PineType.INT
|
|
1408
|
+
|
|
1409
|
+
# shape.* constants (triangleup, triangledown, cross, etc.)
|
|
1410
|
+
if ns == "shape":
|
|
1411
|
+
return PineType.INT
|
|
1412
|
+
|
|
1413
|
+
# location.* constants (belowbar, abovebar, absolute, etc.)
|
|
1414
|
+
if ns == "location":
|
|
1415
|
+
return PineType.INT
|
|
1416
|
+
|
|
1417
|
+
# size.* constants (small, normal, large, etc.)
|
|
1418
|
+
if ns == "size":
|
|
1419
|
+
return PineType.INT
|
|
1420
|
+
|
|
1421
|
+
# currency.* constants (USD, EUR, TWD, etc.)
|
|
1422
|
+
if ns == "currency":
|
|
1423
|
+
return PineType.STRING
|
|
1424
|
+
|
|
1425
|
+
# order.* constants
|
|
1426
|
+
if ns == "order":
|
|
1427
|
+
return PineType.INT
|
|
1428
|
+
|
|
1429
|
+
# format.* constants
|
|
1430
|
+
if ns == "format":
|
|
1431
|
+
return PineType.INT
|
|
1432
|
+
|
|
1433
|
+
# text.* constants (align_left, align_right, etc.)
|
|
1434
|
+
if ns == "text":
|
|
1435
|
+
return PineType.INT
|
|
1436
|
+
|
|
1437
|
+
# extend.* constants (left, right, both, none)
|
|
1438
|
+
if ns == "extend":
|
|
1439
|
+
return PineType.INT
|
|
1440
|
+
|
|
1441
|
+
# xloc.*, yloc.* constants
|
|
1442
|
+
if ns in ("xloc", "yloc"):
|
|
1443
|
+
return PineType.INT
|
|
1444
|
+
|
|
1445
|
+
# label.*, line.*, box.*, table.* methods
|
|
1446
|
+
if ns in ("label", "line", "box", "table"):
|
|
1447
|
+
return PineType.VOID
|
|
1448
|
+
|
|
1449
|
+
# ticker.* functions
|
|
1450
|
+
if ns == "ticker":
|
|
1451
|
+
return PineType.STRING
|
|
1452
|
+
|
|
1453
|
+
# request.* (security, etc.) — skipped but valid
|
|
1454
|
+
if ns == "request":
|
|
1455
|
+
return PineType.FLOAT
|
|
1456
|
+
|
|
1457
|
+
# runtime.* (error, etc.)
|
|
1458
|
+
if ns == "runtime":
|
|
1459
|
+
return PineType.VOID
|
|
1460
|
+
|
|
1461
|
+
# chart.* (fg_color, bg_color, etc.)
|
|
1462
|
+
if ns == "chart":
|
|
1463
|
+
return PineType.COLOR
|
|
1464
|
+
|
|
1465
|
+
# barmerge.* (lookahead_off, gaps_off, etc.)
|
|
1466
|
+
if ns == "barmerge":
|
|
1467
|
+
return PineType.INT
|
|
1468
|
+
|
|
1469
|
+
# adjustment.*, session.* constants
|
|
1470
|
+
if ns in ("adjustment", "session"):
|
|
1471
|
+
return PineType.INT
|
|
1472
|
+
|
|
1473
|
+
# scale.*, font.*, backadjustment.*, settlement_as_close.* constants
|
|
1474
|
+
if ns in ("scale", "font", "backadjustment", "settlement_as_close"):
|
|
1475
|
+
return PineType.INT
|
|
1476
|
+
|
|
1477
|
+
# hline.* constants (style_dashed, style_dotted, etc.)
|
|
1478
|
+
if ns == "hline":
|
|
1479
|
+
return PineType.INT
|
|
1480
|
+
|
|
1481
|
+
# dayofweek.* constants (monday, tuesday, etc.)
|
|
1482
|
+
if ns == "dayofweek":
|
|
1483
|
+
return PineType.INT
|
|
1484
|
+
|
|
1485
|
+
# dividends.*, earnings.*, splits.* variables
|
|
1486
|
+
if ns in ("dividends", "earnings", "splits"):
|
|
1487
|
+
return PineType.FLOAT
|
|
1488
|
+
|
|
1489
|
+
sym = self._symbols.resolve(ns)
|
|
1490
|
+
udt_name = None
|
|
1491
|
+
if sym is not None:
|
|
1492
|
+
udt_name = sym.udt_type_name
|
|
1493
|
+
if udt_name is None and sym.type_spec is not None and sym.type_spec.kind == "udt":
|
|
1494
|
+
udt_name = sym.type_spec.name
|
|
1495
|
+
if udt_name:
|
|
1496
|
+
field_type = (self._udt_fields.get(udt_name) or {}).get(node.member)
|
|
1497
|
+
if field_type is not None:
|
|
1498
|
+
return field_type
|
|
1499
|
+
|
|
1500
|
+
# Handle nested member access (e.g., strategy.oca.reduce,
|
|
1501
|
+
# strategy.closedtrades.profit, strategy.commission.percent)
|
|
1502
|
+
if isinstance(node.object, MemberAccess):
|
|
1503
|
+
owner_spec = self._type_spec_from_expr(node.object)
|
|
1504
|
+
if owner_spec is not None and owner_spec.kind == "udt" and owner_spec.name:
|
|
1505
|
+
field_spec = (self._udt_field_type_specs.get(owner_spec.name) or {}).get(node.member)
|
|
1506
|
+
if field_spec is not None:
|
|
1507
|
+
return self._type_hint_to_pine(str(field_spec))
|
|
1508
|
+
self._visit(node.object)
|
|
1509
|
+
# strategy.closedtrades.* and strategy.opentrades.* return types
|
|
1510
|
+
if (isinstance(node.object.object, Identifier) and
|
|
1511
|
+
node.object.object.name == "strategy"):
|
|
1512
|
+
sub = node.object.member
|
|
1513
|
+
if sub in ("closedtrades", "opentrades"):
|
|
1514
|
+
# .profit, .entry_price, .exit_price, .commission, etc. → FLOAT
|
|
1515
|
+
# .entry_bar_index, .exit_bar_index → INT
|
|
1516
|
+
# .entry_id, .exit_id, .entry_comment, .exit_comment → STRING
|
|
1517
|
+
if node.member in ("entry_bar_index", "exit_bar_index",
|
|
1518
|
+
"first_index"):
|
|
1519
|
+
return PineType.INT
|
|
1520
|
+
if node.member in ("entry_id", "exit_id", "entry_comment",
|
|
1521
|
+
"exit_comment"):
|
|
1522
|
+
return PineType.STRING
|
|
1523
|
+
if node.member in ("entry_time", "exit_time"):
|
|
1524
|
+
return PineType.INT
|
|
1525
|
+
return PineType.FLOAT # profit, entry_price, etc.
|
|
1526
|
+
if sub == "commission":
|
|
1527
|
+
return PineType.INT # strategy.commission.percent, etc.
|
|
1528
|
+
if sub in ("oca", "direction", "risk"):
|
|
1529
|
+
return PineType.INT
|
|
1530
|
+
return PineType.INT
|
|
1531
|
+
|
|
1532
|
+
# General case
|
|
1533
|
+
obj_type = self._visit(node.object)
|
|
1534
|
+
return PineType.UNKNOWN
|
|
1535
|
+
|
|
1536
|
+
def _visit_TypeAnnotation(self, node: TypeAnnotation) -> PineType:
|
|
1537
|
+
return self._type_hint_to_pine(node.type_name)
|
|
1538
|
+
|
|
1539
|
+
# ------------------------------------------------------------------
|
|
1540
|
+
# Literal visitors
|
|
1541
|
+
# ------------------------------------------------------------------
|
|
1542
|
+
|
|
1543
|
+
def _visit_NumberLiteral(self, node: NumberLiteral) -> PineType:
|
|
1544
|
+
if isinstance(node.value, float):
|
|
1545
|
+
return PineType.FLOAT
|
|
1546
|
+
return PineType.INT
|
|
1547
|
+
|
|
1548
|
+
def _visit_StringLiteral(self, node: StringLiteral) -> PineType:
|
|
1549
|
+
return PineType.STRING
|
|
1550
|
+
|
|
1551
|
+
def _visit_BoolLiteral(self, node: BoolLiteral) -> PineType:
|
|
1552
|
+
return PineType.BOOL
|
|
1553
|
+
|
|
1554
|
+
def _visit_NaLiteral(self, node: NaLiteral) -> PineType:
|
|
1555
|
+
return PineType.NA
|
|
1556
|
+
|
|
1557
|
+
def _visit_ColorLiteral(self, node: ColorLiteral) -> PineType:
|
|
1558
|
+
return PineType.COLOR
|
|
1559
|
+
|
|
1560
|
+
def _visit_TupleLiteral(self, node: TupleLiteral) -> PineType:
|
|
1561
|
+
for elem in node.elements:
|
|
1562
|
+
self._visit(elem)
|
|
1563
|
+
return PineType.FLOAT
|