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,1381 @@
|
|
|
1
|
+
"""Code generator: AnalyzerContext -> C++ source for the PineScript backtester.
|
|
2
|
+
|
|
3
|
+
This is the new visitor-pattern codegen that reads pre-computed analysis
|
|
4
|
+
results from AnalyzerContext instead of walking the AST to collect info.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ..ast_nodes import (
|
|
10
|
+
ASTNode, Program, StrategyDecl, VarDecl, Assignment, IfStmt, ForStmt, ForInStmt,
|
|
11
|
+
WhileStmt, SwitchStmt, BreakStmt, ContinueStmt, FuncDef, ExprStmt,
|
|
12
|
+
BinOp, UnaryOp, Ternary, FuncCall, Subscript, Identifier, MemberAccess,
|
|
13
|
+
NumberLiteral, StringLiteral, BoolLiteral, NaLiteral, TupleAssign,
|
|
14
|
+
ColorLiteral, ImportStmt, TupleLiteral,
|
|
15
|
+
TypeDecl, EnumDecl, MethodDef, TypeField,
|
|
16
|
+
)
|
|
17
|
+
from ..analyzer import (
|
|
18
|
+
AnalyzerContext,
|
|
19
|
+
TACallSite,
|
|
20
|
+
FuncInfo,
|
|
21
|
+
FixnanCallSite,
|
|
22
|
+
TA_MULTI_CTOR,
|
|
23
|
+
TA_NO_CTOR,
|
|
24
|
+
TA_PERIOD_ARG,
|
|
25
|
+
)
|
|
26
|
+
from ..symbols import PineType, TypeSpec
|
|
27
|
+
from .. import signatures as sigs
|
|
28
|
+
from ..errors import CompileError, Diagnostic, Level, Phase, SourceLocation
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Mapping tables — definitions live in ``tables.py``; re-imported here so
|
|
32
|
+
# inline references inside this module (BAR_FIELDS[name], MATH_FUNC_MAP[fn],
|
|
33
|
+
# etc.) keep resolving without qualification. The package-level
|
|
34
|
+
# ``__init__.py`` re-exports the same names for external consumers
|
|
35
|
+
# (``support_checker.py`` and external test imports).
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
from .tables import (
|
|
39
|
+
BAR_FIELDS,
|
|
40
|
+
BAR_BUILTINS,
|
|
41
|
+
BAR_SERIES_PUSH,
|
|
42
|
+
SECURITY_OHLC_BAR_FIELDS,
|
|
43
|
+
TA_RETURNS_BOOL,
|
|
44
|
+
TA_IMPLICIT_COMPUTE,
|
|
45
|
+
TA_COMPUTE_ARGS,
|
|
46
|
+
TA_IMPLICIT_COMPUTE_FULL,
|
|
47
|
+
TA_IMPLICIT_APPEND,
|
|
48
|
+
TA_TUPLE_FIELDS,
|
|
49
|
+
PINE_TYPE_TO_CPP,
|
|
50
|
+
SKIP_FUNC_NAMES,
|
|
51
|
+
SKIP_NAMESPACES,
|
|
52
|
+
SKIP_VAR_TYPES,
|
|
53
|
+
SYMINFO_MEMBER_MAP,
|
|
54
|
+
COLOR_CONST_MAP,
|
|
55
|
+
ARRAY_METHODS,
|
|
56
|
+
MAP_METHODS,
|
|
57
|
+
MATRIX_METHODS,
|
|
58
|
+
MATRIX_METHOD_KWARGS,
|
|
59
|
+
MATRIX_NUMERIC_ONLY,
|
|
60
|
+
MATRIX_RETURNING_METHODS,
|
|
61
|
+
MATRIX_SORT_ALLOWED_GENERIC_ELEMS,
|
|
62
|
+
MATH_FUNC_MAP,
|
|
63
|
+
STR_FUNC_MAP,
|
|
64
|
+
_merge_kwargs,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# (TA_IMPLICIT_COMPUTE / TA_COMPUTE_ARGS now imported from .tables above.)
|
|
68
|
+
|
|
69
|
+
# (TA_IMPLICIT_COMPUTE_FULL / TA_IMPLICIT_APPEND / PINE_TYPE_TO_CPP /
|
|
70
|
+
# SKIP_* / SYMINFO_MEMBER_MAP / COLOR_CONST_MAP all imported from .tables.)
|
|
71
|
+
|
|
72
|
+
# (ARRAY_METHODS / MAP_METHODS / MATRIX_METHODS / MATRIX_METHOD_KWARGS /
|
|
73
|
+
# MATH_FUNC_MAP / STR_FUNC_MAP / TA_TUPLE_FIELDS / _matrix_add_row /
|
|
74
|
+
# _matrix_add_col / _merge_kwargs all imported from .tables above.)
|
|
75
|
+
|
|
76
|
+
# Math parameter names live in ``signatures.py`` (sigs.get_param_names).
|
|
77
|
+
|
|
78
|
+
# CPP_RESERVED + the NamingHelper mixin are pulled in from helpers.py so the
|
|
79
|
+
# small naming/walk utilities can be shared with future visitor mixins.
|
|
80
|
+
from .helpers import CPP_RESERVED, NamingHelper
|
|
81
|
+
|
|
82
|
+
# TypeInferer mixin owns the ~15 type-spec / C++-type inference helpers
|
|
83
|
+
# previously scattered across this module; see ``codegen/types.py``.
|
|
84
|
+
from .types import TypeInferer
|
|
85
|
+
|
|
86
|
+
# TaSiteHelper owns site lookup, .compute() arg construction, and the TA
|
|
87
|
+
# hoisting machinery. The runtime-reset chain (_resolve_known and friends)
|
|
88
|
+
# stays on CodeGen for now because it relies on Python's compile-time
|
|
89
|
+
# expression evaluator.
|
|
90
|
+
from .ta import TaSiteHelper
|
|
91
|
+
|
|
92
|
+
# InputHelper owns Pine input.* analysis (defaults, titles, getter dispatch,
|
|
93
|
+
# enum-declared-first guard).
|
|
94
|
+
from .input import InputHelper
|
|
95
|
+
|
|
96
|
+
# SecurityEmitter owns the request.security() lowering pipeline:
|
|
97
|
+
# evaluator emission, dispatch, mutable-global rebind, TA-variant binding
|
|
98
|
+
# stacks, and the per-call helper plan. Most stateful mixin in the
|
|
99
|
+
# package; see its module docstring for the full host-class state contract.
|
|
100
|
+
from .security import SecurityEmitter
|
|
101
|
+
|
|
102
|
+
# TopLevelEmitter owns the top-level C++ section emitters (includes,
|
|
103
|
+
# constructor, on_bar, extern "C") plus the per-function emission helpers
|
|
104
|
+
# (_emit_func_def / _emit_udt_method_cpp_name) used by both regular
|
|
105
|
+
# Pine functions and UDT instance methods.
|
|
106
|
+
from .emit_top import TopLevelEmitter
|
|
107
|
+
|
|
108
|
+
# StmtVisitor owns the statement-level visitors (_visit_stmt dispatcher
|
|
109
|
+
# plus the per-kind handlers for var-decl, assignment, tuple-assign,
|
|
110
|
+
# if/for/while/switch and the if/switch-as-expression lowering).
|
|
111
|
+
from .visit_stmt import StmtVisitor
|
|
112
|
+
from .visit_expr import ExprVisitor
|
|
113
|
+
from .visit_call import CallVisitor
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# CodeGen class
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, NamingHelper):
|
|
121
|
+
"""Generate C++ from an AnalyzerContext (visitor pattern).
|
|
122
|
+
|
|
123
|
+
Mixin chain (Python MRO is left-to-right; method names are
|
|
124
|
+
intentionally kept disjoint across mixins so the order is mostly
|
|
125
|
+
cosmetic):
|
|
126
|
+
* ``CallVisitor`` -- function-call dispatcher (_visit_func_call)
|
|
127
|
+
+ per-namespace dispatch helpers (_visit_strategy_call /
|
|
128
|
+
_visit_color_call / _visit_str_call / _visit_math_call /
|
|
129
|
+
_visit_fixnan) + _resolve_func_args kwarg-merging helper
|
|
130
|
+
* ``ExprVisitor`` -- expression-level visitors (_visit_expr
|
|
131
|
+
dispatcher + per-kind handlers _visit_ident /
|
|
132
|
+
_visit_member_access / _visit_binop / _visit_unaryop /
|
|
133
|
+
_visit_subscript)
|
|
134
|
+
* ``StmtVisitor`` -- statement-level visitors (_visit_stmt
|
|
135
|
+
dispatcher + per-kind handlers + if/switch-as-expression)
|
|
136
|
+
* ``TopLevelEmitter`` -- top-level C++ section emitters
|
|
137
|
+
(includes / constructor / on_bar / extern "C") plus the
|
|
138
|
+
per-function emitters used by Pine functions and UDT methods
|
|
139
|
+
* ``SecurityEmitter`` -- ``request.security()`` lowering pipeline
|
|
140
|
+
(evaluators, dispatch, rebind, TA variants)
|
|
141
|
+
* ``TaSiteHelper`` -- TA call-site lookup + .compute() arg construction + TA hoisting
|
|
142
|
+
* ``TypeInferer`` -- _type_spec_*, _infer_type, _array/_map_method_expr
|
|
143
|
+
* ``InputHelper`` -- Pine ``input.*`` defaults / titles / getter dispatch
|
|
144
|
+
* ``NamingHelper`` -- _safe_name / _resolve_callee / _walk_ast / ...
|
|
145
|
+
|
|
146
|
+
With CallVisitor extracted (step 10/N), the host class is now a thin
|
|
147
|
+
coordinator that keeps state attributes, the constructor, the
|
|
148
|
+
top-level ``generate()`` orchestrator, prescan helpers
|
|
149
|
+
(_collect_known_vars / _find_reassigned_vars / _collect_known_var /
|
|
150
|
+
_prescan_strategy_series), and the runtime-reset chain
|
|
151
|
+
(_resolve_known / _is_skip_expr / _runtime_ctor_arg_for_reset /
|
|
152
|
+
_collect_ta_runtime_resets / _emit_ta_runtime_reset) — kept here
|
|
153
|
+
because the chain relies on Python's compile-time expression
|
|
154
|
+
evaluator.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, ctx: AnalyzerContext) -> None:
|
|
158
|
+
self.ctx = ctx
|
|
159
|
+
# Build lookup: node id -> TACallSite (only for non-function-local sites)
|
|
160
|
+
self._ta_site_map: dict[int, TACallSite] = {}
|
|
161
|
+
# Build per-call-site TA member name remapping for user functions
|
|
162
|
+
# Maps (func_name, cs_idx) -> {original_member_name: cloned_member_name}
|
|
163
|
+
self._func_cs_ta_remap: dict[tuple[str, int], dict[str, str]] = {}
|
|
164
|
+
# Active TA name remap (set during per-call-site function emission)
|
|
165
|
+
self._active_ta_remap: dict[str, str] = {}
|
|
166
|
+
# Flag: inside a per-call-site function variant (enables TA hoisting)
|
|
167
|
+
self._in_ta_func_variant: bool = False
|
|
168
|
+
# Active call-site index (set during per-call-site function emission)
|
|
169
|
+
self._active_call_site_idx: int | None = None
|
|
170
|
+
# Set of TA member names that belong to user functions
|
|
171
|
+
self._func_ta_members: set[str] = set()
|
|
172
|
+
|
|
173
|
+
# Build per-call-site var/series member name remapping for user functions
|
|
174
|
+
# Maps (func_name, cs_idx) -> {original_var_name: cloned_var_name}
|
|
175
|
+
self._func_cs_var_remap: dict[tuple[str, int], dict[str, str]] = {}
|
|
176
|
+
# Active var name remap (set during per-call-site function emission)
|
|
177
|
+
self._active_var_remap: dict[str, str] = {}
|
|
178
|
+
# Set of var/series member names that belong to user functions (need cloning)
|
|
179
|
+
self._func_var_members_set: set[str] = set()
|
|
180
|
+
self._precalc_loop_active: bool = False
|
|
181
|
+
|
|
182
|
+
# Build per-function var/series name lists for cloning.
|
|
183
|
+
# For each function with call-site variants, collect ALL function-scoped
|
|
184
|
+
# series vars (from this function AND any sub-functions it calls).
|
|
185
|
+
# This ensures sub-function series vars get cloned for the parent's call sites.
|
|
186
|
+
func_var_originals: dict[str, list[str]] = {} # func_name -> list of original var names
|
|
187
|
+
|
|
188
|
+
# First, collect all function-scoped series vars (union across all functions)
|
|
189
|
+
all_func_scoped_series: set[str] = set()
|
|
190
|
+
for svars in ctx.func_series_vars.values():
|
|
191
|
+
all_func_scoped_series.update(svars)
|
|
192
|
+
# Also include function-scoped var_members
|
|
193
|
+
all_func_scoped_vars: set[str] = set()
|
|
194
|
+
for vlist in ctx.func_var_members.values():
|
|
195
|
+
for n, _, _ in vlist:
|
|
196
|
+
all_func_scoped_vars.add(n)
|
|
197
|
+
|
|
198
|
+
# For each function with call-site cloning (has TA ranges or is called multiple times),
|
|
199
|
+
# include ALL function-scoped series/var vars that could be used in its body
|
|
200
|
+
for fname in set(ctx.func_call_site_counts.keys()):
|
|
201
|
+
total_cs = ctx.func_call_site_counts[fname]
|
|
202
|
+
if total_cs <= 1:
|
|
203
|
+
continue # No cloning needed for single-call-site functions
|
|
204
|
+
orig_names: list[str] = []
|
|
205
|
+
# Include function's own vars
|
|
206
|
+
if fname in ctx.func_var_members:
|
|
207
|
+
for n, _, _ in ctx.func_var_members[fname]:
|
|
208
|
+
if n not in orig_names:
|
|
209
|
+
orig_names.append(n)
|
|
210
|
+
# Include function's own series vars
|
|
211
|
+
if fname in ctx.func_series_vars:
|
|
212
|
+
for sv in ctx.func_series_vars[fname]:
|
|
213
|
+
if sv not in orig_names:
|
|
214
|
+
orig_names.append(sv)
|
|
215
|
+
# Include series vars from sub-functions (they share the same class members)
|
|
216
|
+
for sv in all_func_scoped_series:
|
|
217
|
+
if sv not in orig_names:
|
|
218
|
+
orig_names.append(sv)
|
|
219
|
+
for sv in all_func_scoped_vars:
|
|
220
|
+
if sv not in orig_names:
|
|
221
|
+
orig_names.append(sv)
|
|
222
|
+
if orig_names:
|
|
223
|
+
func_var_originals[fname] = orig_names
|
|
224
|
+
self._func_var_members_set.update(orig_names)
|
|
225
|
+
# cs0 uses originals (identity mapping)
|
|
226
|
+
self._func_cs_var_remap[(fname, 0)] = {self._safe_name(n): self._safe_name(n) for n in orig_names}
|
|
227
|
+
|
|
228
|
+
# Build cloned var remapping for cs > 0
|
|
229
|
+
for fname, orig_names in func_var_originals.items():
|
|
230
|
+
total_cs = ctx.func_call_site_counts.get(fname, 1)
|
|
231
|
+
for cs_idx in range(1, total_cs):
|
|
232
|
+
remap = {}
|
|
233
|
+
for orig_name in orig_names:
|
|
234
|
+
safe = self._safe_name(orig_name)
|
|
235
|
+
remap[safe] = f"{safe}_cs{cs_idx}"
|
|
236
|
+
self._func_cs_var_remap[(fname, cs_idx)] = remap
|
|
237
|
+
self._func_var_members_set.update(
|
|
238
|
+
orig_name for orig_name in orig_names)
|
|
239
|
+
|
|
240
|
+
# Build TA site map and per-call-site remapping
|
|
241
|
+
func_ta_originals: dict[str, list[str]] = {} # func_name -> list of original member names
|
|
242
|
+
for fname, (start, end) in ctx.func_ta_ranges.items():
|
|
243
|
+
orig_names = [ctx.ta_call_sites[i].member_name for i in range(start, end)]
|
|
244
|
+
func_ta_originals[fname] = orig_names
|
|
245
|
+
self._func_ta_members.update(orig_names)
|
|
246
|
+
# cs0 uses originals (identity mapping)
|
|
247
|
+
self._func_cs_ta_remap[(fname, 0)] = {n: n for n in orig_names}
|
|
248
|
+
|
|
249
|
+
# Build cloned site remapping for cs > 0 (must happen before _ta_site_map
|
|
250
|
+
# so cloned names are in _func_ta_members and get filtered out of the map)
|
|
251
|
+
for fname, orig_names in func_ta_originals.items():
|
|
252
|
+
total_cs = ctx.func_call_site_counts.get(fname, 1)
|
|
253
|
+
for cs_idx in range(1, total_cs):
|
|
254
|
+
remap = {}
|
|
255
|
+
for orig_name in orig_names:
|
|
256
|
+
remap[orig_name] = f"{orig_name}_cs{cs_idx}"
|
|
257
|
+
self._func_cs_ta_remap[(fname, cs_idx)] = remap
|
|
258
|
+
self._func_ta_members.update(remap.values())
|
|
259
|
+
|
|
260
|
+
for site in ctx.ta_call_sites:
|
|
261
|
+
if site.node is not None:
|
|
262
|
+
if site.member_name not in self._func_ta_members:
|
|
263
|
+
self._ta_site_map[id(site.node)] = site
|
|
264
|
+
elif not any(site.member_name.endswith(f"_cs{i}") for i in range(1, 100)):
|
|
265
|
+
# Original (cs0) function-local site — add to map for initial visit
|
|
266
|
+
self._ta_site_map[id(site.node)] = site
|
|
267
|
+
self._ta_index_by_site_id: dict[int, int] = {
|
|
268
|
+
id(site): i for i, site in enumerate(ctx.ta_call_sites)
|
|
269
|
+
}
|
|
270
|
+
# Build lookup: node id -> FixnanCallSite (counter-based)
|
|
271
|
+
self._fixnan_counter = 0
|
|
272
|
+
self._switch_counter = 0
|
|
273
|
+
self._security_inline_counter = 0
|
|
274
|
+
self._random_call_counter = 0
|
|
275
|
+
# UDT / enum (needed before _collect_known_vars for input.enum)
|
|
276
|
+
self._udt_defs: dict[str, list] = {}
|
|
277
|
+
self._enum_defs: dict[str, list[str]] = {}
|
|
278
|
+
for stmt in ctx.ast.body:
|
|
279
|
+
if isinstance(stmt, TypeDecl):
|
|
280
|
+
self._udt_defs[stmt.name] = stmt.fields
|
|
281
|
+
if isinstance(stmt, EnumDecl):
|
|
282
|
+
self._enum_defs[stmt.name] = stmt.members
|
|
283
|
+
self._enum_member_strings: dict[str, list[str]] = getattr(
|
|
284
|
+
ctx, "enum_member_strings", None
|
|
285
|
+
) or {}
|
|
286
|
+
# Contextual var name for input title fallback (set during _visit_var_decl)
|
|
287
|
+
self._current_input_var_name: str | None = None
|
|
288
|
+
# Build known_vars for constant propagation
|
|
289
|
+
self._known_vars: dict[str, int | float | bool | str] = {}
|
|
290
|
+
# Subset of _known_vars whose value came from an input.*() call. These
|
|
291
|
+
# MUST NOT be inlined at identifier use sites because strategy_set_input()
|
|
292
|
+
# can override them at runtime. Ctor-time uses (TA buffer sizing,
|
|
293
|
+
# request.security TF) use the Pine default at construction but then get
|
|
294
|
+
# rebuilt on first on_bar via _emit_ta_runtime_reset().
|
|
295
|
+
self._input_backed_vars: set[str] = set()
|
|
296
|
+
# Map input-backed var name -> its input.*() FuncCall node so we can
|
|
297
|
+
# later emit a runtime get_input_*() read with the same title/default.
|
|
298
|
+
self._input_var_to_call: dict[str, FuncCall] = {}
|
|
299
|
+
self._timeframe_period_vars: set[str] = set()
|
|
300
|
+
self._collect_known_vars()
|
|
301
|
+
# Track var names
|
|
302
|
+
self._var_names: set[str] = set()
|
|
303
|
+
for name, _, _ in ctx.var_members:
|
|
304
|
+
self._var_names.add(name)
|
|
305
|
+
# Every name bound ANYWHERE in the program (top-level, nested in
|
|
306
|
+
# if/for/while/switch blocks, or inside function bodies). The
|
|
307
|
+
# unknown-identifier guard in _visit_ident uses this as a generous
|
|
308
|
+
# last-resort allow-list: a name bound nowhere AND not a builtin is a
|
|
309
|
+
# genuinely-undefined read that would emit an undeclared C++ symbol.
|
|
310
|
+
# Block-scoped locals (e.g. a var declared inside an on_bar for-loop)
|
|
311
|
+
# are otherwise invisible to the per-scope tracking sets.
|
|
312
|
+
self._all_bound_names: set[str] = self._collect_binding_names(ctx.ast.body)
|
|
313
|
+
# Build set of user-defined function names and lookup map
|
|
314
|
+
self._func_names: set[str] = set()
|
|
315
|
+
self._func_info_map: dict[str, FuncInfo] = {}
|
|
316
|
+
for fi in ctx.func_infos:
|
|
317
|
+
self._func_names.add(fi.name)
|
|
318
|
+
self._func_info_map[fi.name] = fi
|
|
319
|
+
# Track strategy series vars (e.g., strategy.closedtrades[1])
|
|
320
|
+
self._strategy_series_vars: set[str] = set()
|
|
321
|
+
# Track global-scope non-var declarations (emitted as class members)
|
|
322
|
+
self._global_member_vars: set[str] = set()
|
|
323
|
+
for name, _ in ctx.global_var_decls:
|
|
324
|
+
self._global_member_vars.add(name)
|
|
325
|
+
self._global_mutable_infos: dict[str, object] = getattr(ctx, "global_mutable_infos", {}) or {}
|
|
326
|
+
self._udt_var_types: dict[str, str] = getattr(ctx, "udt_var_types", {}) or {}
|
|
327
|
+
self._collection_types: dict[str, TypeSpec] = getattr(ctx, "collection_types", {}) or {}
|
|
328
|
+
self._udt_field_type_specs: dict[str, dict[str, TypeSpec]] = getattr(ctx, "udt_field_type_specs", {}) or {}
|
|
329
|
+
# Map UDT struct name -> set of field names that were dropped from the
|
|
330
|
+
# emitted C++ struct because they had drawing-only types (label, line,
|
|
331
|
+
# box, linefill, polyline, table, chart.point). Populated eagerly
|
|
332
|
+
# here from ``self._udt_defs`` so downstream visitors (visit_expr /
|
|
333
|
+
# visit_stmt) can consult it before ``generate()`` runs. The struct
|
|
334
|
+
# emission loop later asserts/syncs against this same map. Used to
|
|
335
|
+
# rewrite or strip downstream references to those fields so the
|
|
336
|
+
# generated C++ never references a member that doesn't exist on the
|
|
337
|
+
# emitted struct. See: pineforge-codegen issue #10.
|
|
338
|
+
_DRAWING_TYPES_INIT = {"label", "line", "box", "table", "linefill", "polyline", "chart.point"}
|
|
339
|
+
self._udt_omitted_fields: dict[str, set[str]] = {}
|
|
340
|
+
for _type_name, _fields in self._udt_defs.items():
|
|
341
|
+
_omitted = set()
|
|
342
|
+
for _f in _fields:
|
|
343
|
+
if _f.type_name and _f.type_name in _DRAWING_TYPES_INIT:
|
|
344
|
+
_omitted.add(_f.name)
|
|
345
|
+
self._udt_omitted_fields[_type_name] = _omitted
|
|
346
|
+
self._udt_param_udt: dict[str, str] = {}
|
|
347
|
+
self._security_calls: list[dict] = [self._normalize_security_call(item) for item in ctx.security_calls]
|
|
348
|
+
# Current function parameter types (set during _emit_func_def)
|
|
349
|
+
self._current_func_param_types: dict[str, str] = {}
|
|
350
|
+
# Current function params that are series (const Series<double>&)
|
|
351
|
+
self._current_func_series_params: set[str] = set()
|
|
352
|
+
# Locals declared in the function currently being emitted (symbol table loses them after analysis)
|
|
353
|
+
self._current_func_locals: set[str] = set()
|
|
354
|
+
# for-in loop iterator names (must resolve member access, not enum fallback)
|
|
355
|
+
self._current_loop_vars: set[str] = set()
|
|
356
|
+
# Track array variables for codegen
|
|
357
|
+
self._array_vars: set[str] = set()
|
|
358
|
+
# Track map variables for codegen
|
|
359
|
+
self._map_vars: set[str] = set()
|
|
360
|
+
# Track matrix variables for codegen (name -> TypeSpec)
|
|
361
|
+
self._matrix_specs: dict[str, "TypeSpec"] = {}
|
|
362
|
+
for _name, _spec in self._collection_types.items():
|
|
363
|
+
if _spec.kind == "array":
|
|
364
|
+
self._array_vars.add(_name)
|
|
365
|
+
elif _spec.kind == "map":
|
|
366
|
+
self._map_vars.add(_name)
|
|
367
|
+
elif _spec.kind == "udt" and _spec.name:
|
|
368
|
+
self._udt_var_types.setdefault(_name, _spec.name)
|
|
369
|
+
# Collect request.security metadata per call
|
|
370
|
+
self._security_eval_info: list[dict] = []
|
|
371
|
+
self._security_ta_variant_names: dict[tuple[int, int, tuple], str] = {}
|
|
372
|
+
for item in self._security_calls:
|
|
373
|
+
sec_id = item["sec_id"]
|
|
374
|
+
tf_node = item["tf_node"]
|
|
375
|
+
gaps_node = item.get("gaps_node")
|
|
376
|
+
lookahead_node = item.get("lookahead_node")
|
|
377
|
+
ta_range = item.get("ta_range")
|
|
378
|
+
|
|
379
|
+
tf_str = None
|
|
380
|
+
if isinstance(tf_node, StringLiteral):
|
|
381
|
+
tf_str = tf_node.value
|
|
382
|
+
elif (isinstance(tf_node, Identifier)
|
|
383
|
+
and tf_node.name in self._known_vars
|
|
384
|
+
and tf_node.name not in self._input_backed_vars):
|
|
385
|
+
val = self._known_vars[tf_node.name]
|
|
386
|
+
if isinstance(val, str):
|
|
387
|
+
tf_str = val
|
|
388
|
+
|
|
389
|
+
is_lookahead_on = False
|
|
390
|
+
if lookahead_node is not None:
|
|
391
|
+
if isinstance(lookahead_node, MemberAccess) and lookahead_node.member == "lookahead_on":
|
|
392
|
+
is_lookahead_on = True
|
|
393
|
+
|
|
394
|
+
is_gaps_on = False
|
|
395
|
+
if gaps_node is not None:
|
|
396
|
+
if isinstance(gaps_node, MemberAccess) and gaps_node.member == "gaps_on":
|
|
397
|
+
is_gaps_on = True
|
|
398
|
+
|
|
399
|
+
expr_node = item["expr_node"]
|
|
400
|
+
inline_helper_ta_indices: set[int] = set()
|
|
401
|
+
ta_binding_stacks = self._collect_security_ta_binding_stacks(
|
|
402
|
+
expr_node,
|
|
403
|
+
inline_ta_indices=inline_helper_ta_indices,
|
|
404
|
+
)
|
|
405
|
+
ta_indices = self._collect_security_ta_indices(expr_node)
|
|
406
|
+
ta_variants: dict[int, list[dict]] = {}
|
|
407
|
+
for idx in sorted(ta_indices):
|
|
408
|
+
site = self.ctx.ta_call_sites[idx]
|
|
409
|
+
binding_map = ta_binding_stacks.get(idx) or {(): ()}
|
|
410
|
+
signatures = sorted(binding_map.keys(), key=repr)
|
|
411
|
+
use_base_name = len(signatures) == 1
|
|
412
|
+
variants: list[dict] = []
|
|
413
|
+
for variant_idx, signature in enumerate(signatures):
|
|
414
|
+
member_name = (
|
|
415
|
+
f"_sec{sec_id}_{site.member_name}"
|
|
416
|
+
if use_base_name
|
|
417
|
+
else f"_sec{sec_id}_{site.member_name}_v{variant_idx}"
|
|
418
|
+
)
|
|
419
|
+
result_name = (
|
|
420
|
+
f"_secval_{idx}"
|
|
421
|
+
if use_base_name
|
|
422
|
+
else f"_secval_{idx}_v{variant_idx}"
|
|
423
|
+
)
|
|
424
|
+
binding_stack = binding_map[signature]
|
|
425
|
+
variants.append(
|
|
426
|
+
{
|
|
427
|
+
"signature": signature,
|
|
428
|
+
"binding_stack": binding_stack,
|
|
429
|
+
"member_name": member_name,
|
|
430
|
+
"result_name": result_name,
|
|
431
|
+
}
|
|
432
|
+
)
|
|
433
|
+
self._security_ta_variant_names[(sec_id, idx, signature)] = member_name
|
|
434
|
+
ta_variants[idx] = variants
|
|
435
|
+
self._security_eval_info.append({
|
|
436
|
+
"sec_id": sec_id,
|
|
437
|
+
"tf": tf_str,
|
|
438
|
+
"tf_node": tf_node,
|
|
439
|
+
"gaps_on": is_gaps_on,
|
|
440
|
+
"lookahead_on": is_lookahead_on,
|
|
441
|
+
"ta_range": ta_range,
|
|
442
|
+
"ta_indices": sorted(ta_indices),
|
|
443
|
+
"ta_binding_stacks": ta_binding_stacks,
|
|
444
|
+
"ta_variants": ta_variants,
|
|
445
|
+
"inline_helper_ta_indices": sorted(inline_helper_ta_indices),
|
|
446
|
+
"depends_on_mutable_globals": item.get("depends_on_mutable_globals", False),
|
|
447
|
+
"mutable_globals": list(item.get("mutable_globals", [])),
|
|
448
|
+
"is_lower_tf_array": bool(item.get("is_lower_tf_array", False)),
|
|
449
|
+
})
|
|
450
|
+
# Build set of all member names (series vars, var members) for collision detection
|
|
451
|
+
self._all_member_names: set[str] = set()
|
|
452
|
+
for name in ctx.series_vars:
|
|
453
|
+
self._all_member_names.add(self._safe_name(name))
|
|
454
|
+
for name, _, _ in ctx.var_members:
|
|
455
|
+
self._all_member_names.add(self._safe_name(name))
|
|
456
|
+
|
|
457
|
+
self._register_global_aggregate_member_types()
|
|
458
|
+
self._uses_matrix = self._detect_matrix_usage()
|
|
459
|
+
|
|
460
|
+
def _register_global_aggregate_member_types(self) -> None:
|
|
461
|
+
"""Infer matrix/array/map class members for global non-var declarations from RHS AST.
|
|
462
|
+
|
|
463
|
+
``var m = matrix.new(...)`` is covered by the ``var_members`` emission loop.
|
|
464
|
+
A global ``m = matrix.new(...)`` only appears in ``global_var_decls`` and
|
|
465
|
+
``global_expr_map``; without registering it here, ``m`` was emitted as a scalar
|
|
466
|
+
while ``on_bar`` still assigned ``PineMatrix``.
|
|
467
|
+
"""
|
|
468
|
+
gem = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
469
|
+
for name, _ptype in self.ctx.global_var_decls:
|
|
470
|
+
expr = gem.get(name)
|
|
471
|
+
if expr is None or not isinstance(expr, FuncCall):
|
|
472
|
+
continue
|
|
473
|
+
fn, ns = self._resolve_callee(expr.callee)
|
|
474
|
+
if ns == "matrix" and fn is not None and (
|
|
475
|
+
fn == "new"
|
|
476
|
+
# Methods like ``inv`` / ``pinv`` / ``transpose`` / ``copy`` /
|
|
477
|
+
# ``submatrix`` / ``concat`` / ``diff`` / ``mult`` / ``pow`` /
|
|
478
|
+
# ``eigenvectors`` / ``kron`` return a ``PineMatrix`` from the
|
|
479
|
+
# runtime. Without this branch the LHS variable falls through
|
|
480
|
+
# to the analyzer's default ``double`` and the emitted C++
|
|
481
|
+
# fails to compile (``double = PineMatrix``).
|
|
482
|
+
or fn in MATRIX_RETURNING_METHODS
|
|
483
|
+
):
|
|
484
|
+
if fn == "new":
|
|
485
|
+
targs = self._template_args_from_call(expr) if hasattr(expr, "annotations") else []
|
|
486
|
+
elem_spec = self._type_spec_from_hint_name(targs[0]) if targs else TypeSpec.primitive("float")
|
|
487
|
+
spec = TypeSpec.matrix(elem_spec)
|
|
488
|
+
else:
|
|
489
|
+
recv_name = self._extract_receiver_name(expr)
|
|
490
|
+
spec = self._matrix_specs.get(recv_name) or TypeSpec.matrix(TypeSpec.primitive("float"))
|
|
491
|
+
self._matrix_specs[name] = spec
|
|
492
|
+
self._collection_types[name] = spec
|
|
493
|
+
elif ns == "array" and fn in (
|
|
494
|
+
"new",
|
|
495
|
+
"new_float",
|
|
496
|
+
"new_int",
|
|
497
|
+
"new_bool",
|
|
498
|
+
"new_string",
|
|
499
|
+
"from",
|
|
500
|
+
):
|
|
501
|
+
self._array_vars.add(name)
|
|
502
|
+
elif ns == "map" and fn == "new":
|
|
503
|
+
self._map_vars.add(name)
|
|
504
|
+
|
|
505
|
+
# Also register var/varip matrix members from AST nodes so that
|
|
506
|
+
# the typed-matrix gate checks see the correct element spec.
|
|
507
|
+
var_decl_map: dict[str, FuncCall] = {}
|
|
508
|
+
for stmt in (self.ctx.ast.body if hasattr(self.ctx, "ast") else []):
|
|
509
|
+
if isinstance(stmt, VarDecl) and isinstance(stmt.value, FuncCall):
|
|
510
|
+
var_decl_map[stmt.name] = stmt.value
|
|
511
|
+
for name, _ptype, _init_str in self.ctx.var_members:
|
|
512
|
+
if name in self._matrix_specs:
|
|
513
|
+
continue
|
|
514
|
+
expr = var_decl_map.get(name)
|
|
515
|
+
if expr is None:
|
|
516
|
+
continue
|
|
517
|
+
fn2, ns2 = self._resolve_callee(expr.callee)
|
|
518
|
+
if ns2 == "matrix" and fn2 == "new":
|
|
519
|
+
targs2 = self._template_args_from_call(expr) if hasattr(expr, "annotations") else []
|
|
520
|
+
elem_spec2 = self._type_spec_from_hint_name(targs2[0]) if targs2 else TypeSpec.primitive("float")
|
|
521
|
+
spec2 = TypeSpec.matrix(elem_spec2)
|
|
522
|
+
self._matrix_specs[name] = spec2
|
|
523
|
+
self._collection_types[name] = spec2
|
|
524
|
+
else:
|
|
525
|
+
# Chained matrix-returning calls (e.g. ``var m2 = m.transpose().copy()``).
|
|
526
|
+
# The outer callee is a MemberAccess whose member is in
|
|
527
|
+
# MATRIX_RETURNING_METHODS; walk back to the source receiver so m2
|
|
528
|
+
# inherits the source's element type.
|
|
529
|
+
outer_callee = expr.callee
|
|
530
|
+
if (
|
|
531
|
+
isinstance(outer_callee, MemberAccess)
|
|
532
|
+
and outer_callee.member in MATRIX_RETURNING_METHODS
|
|
533
|
+
):
|
|
534
|
+
recv_name2 = self._extract_receiver_name(expr)
|
|
535
|
+
if recv_name2 is not None and recv_name2 in self._matrix_specs:
|
|
536
|
+
spec2 = self._matrix_specs[recv_name2]
|
|
537
|
+
self._matrix_specs[name] = spec2
|
|
538
|
+
self._collection_types[name] = spec2
|
|
539
|
+
|
|
540
|
+
def _extract_receiver_name(self, call_node) -> str | None:
|
|
541
|
+
"""Extract receiver Identifier name from m.method(...) or matrix.method(m, ...).
|
|
542
|
+
|
|
543
|
+
Walks chained ``FuncCall`` receivers (e.g. ``m.transpose().copy()``)
|
|
544
|
+
until it finds an ``Identifier`` so the source matrix's TypeSpec can
|
|
545
|
+
be propagated through fluent call chains.
|
|
546
|
+
"""
|
|
547
|
+
if not isinstance(call_node, FuncCall):
|
|
548
|
+
return None
|
|
549
|
+
callee = call_node.callee
|
|
550
|
+
# Method form: m.method(...) — possibly chained: m.foo().bar()
|
|
551
|
+
if isinstance(callee, MemberAccess):
|
|
552
|
+
obj = callee.object
|
|
553
|
+
# Walk through nested FuncCall.callee.object chains.
|
|
554
|
+
while isinstance(obj, FuncCall):
|
|
555
|
+
inner_callee = obj.callee
|
|
556
|
+
if isinstance(inner_callee, MemberAccess):
|
|
557
|
+
obj = inner_callee.object
|
|
558
|
+
else:
|
|
559
|
+
break
|
|
560
|
+
if isinstance(obj, Identifier):
|
|
561
|
+
if obj.name != "matrix":
|
|
562
|
+
return obj.name
|
|
563
|
+
# matrix.method(m, ...) functional form
|
|
564
|
+
if call_node.args:
|
|
565
|
+
first = call_node.args[0]
|
|
566
|
+
if isinstance(first, Identifier):
|
|
567
|
+
return first.name
|
|
568
|
+
return None
|
|
569
|
+
|
|
570
|
+
def _check_matrix_method_allowed(self, meth_name, recv_spec, node) -> None:
|
|
571
|
+
"""Validate matrix method against the receiver's element TypeSpec.
|
|
572
|
+
|
|
573
|
+
Centralises two gates that previously lived inline at three call sites
|
|
574
|
+
in ``visit_call.py``:
|
|
575
|
+
|
|
576
|
+
* Numeric-only methods (``det``, ``inv``, ``sum``, …) require
|
|
577
|
+
``matrix<float>``.
|
|
578
|
+
* ``sort`` requires a primitive element (``int``/``bool``/``string``/
|
|
579
|
+
``float``); UDT element types are rejected.
|
|
580
|
+
|
|
581
|
+
Errors are routed through :py:meth:`_codegen_error` so the diagnostic
|
|
582
|
+
format matches the rest of the codegen.
|
|
583
|
+
"""
|
|
584
|
+
if recv_spec is None or recv_spec.kind != "matrix":
|
|
585
|
+
return
|
|
586
|
+
elem = recv_spec.element
|
|
587
|
+
if meth_name in MATRIX_NUMERIC_ONLY:
|
|
588
|
+
if not (elem.kind == "primitive" and elem.name == "float"):
|
|
589
|
+
elem_str = self._type_spec_to_cpp(elem)
|
|
590
|
+
self._codegen_error(
|
|
591
|
+
node,
|
|
592
|
+
f"matrix.{meth_name} requires matrix<float>; got matrix<{elem_str}>",
|
|
593
|
+
hint="Numeric-only methods are not available for matrix<int>, matrix<bool>, matrix<string>, or matrix<UDT>.",
|
|
594
|
+
)
|
|
595
|
+
if meth_name == "sort":
|
|
596
|
+
if elem.kind == "primitive":
|
|
597
|
+
if elem.name not in MATRIX_SORT_ALLOWED_GENERIC_ELEMS and elem.name != "float":
|
|
598
|
+
self._codegen_error(node, f"matrix.sort requires int, bool, string, or float element type; got {elem.name}")
|
|
599
|
+
else:
|
|
600
|
+
self._codegen_error(node, "matrix.sort requires int, bool, string, or float element type; UDT matrices cannot be sorted")
|
|
601
|
+
|
|
602
|
+
def _detect_matrix_usage(self) -> bool:
|
|
603
|
+
"""True if emitted C++ will need runtime/matrix.hpp (PineMatrix)."""
|
|
604
|
+
for _, _, init_str in self.ctx.var_members:
|
|
605
|
+
if init_str and "matrix.new" in str(init_str):
|
|
606
|
+
return True
|
|
607
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
608
|
+
if isinstance(node, FuncCall):
|
|
609
|
+
_fn, ns = self._resolve_callee(node.callee)
|
|
610
|
+
if ns == "matrix":
|
|
611
|
+
return True
|
|
612
|
+
return False
|
|
613
|
+
|
|
614
|
+
# The type-inference helpers (_type_spec_*, _infer_type, _array_method_expr,
|
|
615
|
+
# _map_method_expr, _template_args_from_call, ...) live on TypeInferer
|
|
616
|
+
# — see codegen/types.py.
|
|
617
|
+
|
|
618
|
+
# _security_* / _emit_security_* / _build_security_expr / _normalize_security_call /
|
|
619
|
+
# _rewrite_security_cpp / _collect_security_* / _expr_depends_on_security_mutables /
|
|
620
|
+
# _emit_security_linear_helper_call / _literal_int_for_security_index live on
|
|
621
|
+
# SecurityEmitter (codegen/security.py).
|
|
622
|
+
|
|
623
|
+
def _merge_ta_call_args(self, func_name: str, node: FuncCall) -> list:
|
|
624
|
+
param_names = sigs.get_param_names("ta", func_name)
|
|
625
|
+
if param_names is None and func_name == "sum":
|
|
626
|
+
param_names = sigs.get_param_names("math", "sum")
|
|
627
|
+
|
|
628
|
+
all_args = list(node.args)
|
|
629
|
+
if param_names:
|
|
630
|
+
for i, pname in enumerate(param_names):
|
|
631
|
+
if pname in node.kwargs:
|
|
632
|
+
while len(all_args) <= i:
|
|
633
|
+
all_args.append(None)
|
|
634
|
+
all_args[i] = node.kwargs[pname]
|
|
635
|
+
|
|
636
|
+
if func_name == "highest" and len(all_args) == 1:
|
|
637
|
+
all_args = [Identifier(name="high"), all_args[0]]
|
|
638
|
+
elif func_name == "lowest" and len(all_args) == 1:
|
|
639
|
+
all_args = [Identifier(name="low"), all_args[0]]
|
|
640
|
+
|
|
641
|
+
return all_args
|
|
642
|
+
|
|
643
|
+
def _collect_known_vars(self) -> None:
|
|
644
|
+
"""Collect known constant values from the AST for constant propagation."""
|
|
645
|
+
# First, find all variables that are reassigned anywhere in the AST.
|
|
646
|
+
# These cannot be inlined as constants since their value changes at runtime.
|
|
647
|
+
reassigned = self._find_reassigned_vars()
|
|
648
|
+
for stmt in self.ctx.ast.body:
|
|
649
|
+
if isinstance(stmt, VarDecl) and stmt.name not in reassigned:
|
|
650
|
+
self._collect_known_var(stmt)
|
|
651
|
+
|
|
652
|
+
def _find_reassigned_vars(self) -> set[str]:
|
|
653
|
+
"""Scan AST to find all variable names that are targets of := or compound assignment."""
|
|
654
|
+
reassigned: set[str] = set()
|
|
655
|
+
def walk(node):
|
|
656
|
+
if isinstance(node, Assignment):
|
|
657
|
+
if isinstance(node.target, Identifier):
|
|
658
|
+
reassigned.add(node.target.name)
|
|
659
|
+
# Recurse into child nodes
|
|
660
|
+
if hasattr(node, 'body') and isinstance(node.body, list):
|
|
661
|
+
for child in node.body:
|
|
662
|
+
walk(child)
|
|
663
|
+
if hasattr(node, 'else_body') and isinstance(node.else_body, list):
|
|
664
|
+
for child in node.else_body:
|
|
665
|
+
walk(child)
|
|
666
|
+
if hasattr(node, 'cases') and isinstance(node.cases, list):
|
|
667
|
+
for expr, stmts in node.cases:
|
|
668
|
+
for child in stmts:
|
|
669
|
+
walk(child)
|
|
670
|
+
if hasattr(node, 'default_body') and isinstance(node.default_body, list):
|
|
671
|
+
for child in node.default_body:
|
|
672
|
+
walk(child)
|
|
673
|
+
for stmt in self.ctx.ast.body:
|
|
674
|
+
walk(stmt)
|
|
675
|
+
return reassigned
|
|
676
|
+
|
|
677
|
+
def _collect_known_var(self, node: VarDecl) -> None:
|
|
678
|
+
"""Extract known constant value from a VarDecl."""
|
|
679
|
+
# Don't inline series variables — their values change over time
|
|
680
|
+
if node.name in self.ctx.series_vars:
|
|
681
|
+
return
|
|
682
|
+
# Don't inline var/varip variables — they're mutable state that persists
|
|
683
|
+
# across bars and can be reassigned with :=
|
|
684
|
+
if node.is_var or node.is_varip:
|
|
685
|
+
return
|
|
686
|
+
if isinstance(node.value, NumberLiteral):
|
|
687
|
+
self._known_vars[node.name] = node.value.value
|
|
688
|
+
elif isinstance(node.value, BoolLiteral):
|
|
689
|
+
self._known_vars[node.name] = node.value.value
|
|
690
|
+
elif isinstance(node.value, StringLiteral):
|
|
691
|
+
self._known_vars[node.name] = node.value.value
|
|
692
|
+
elif isinstance(node.value, Identifier):
|
|
693
|
+
if node.value.name in self._known_vars:
|
|
694
|
+
self._known_vars[node.name] = self._known_vars[node.value.name]
|
|
695
|
+
if node.value.name in self._input_backed_vars:
|
|
696
|
+
self._input_backed_vars.add(node.name)
|
|
697
|
+
if node.value.name in self._input_var_to_call:
|
|
698
|
+
self._input_var_to_call[node.name] = self._input_var_to_call[node.value.name]
|
|
699
|
+
if node.value.name in self._timeframe_period_vars:
|
|
700
|
+
self._timeframe_period_vars.add(node.name)
|
|
701
|
+
elif (isinstance(node.value, MemberAccess)
|
|
702
|
+
and isinstance(node.value.object, Identifier)
|
|
703
|
+
and node.value.object.name == "timeframe"
|
|
704
|
+
and node.value.member == "period"):
|
|
705
|
+
self._timeframe_period_vars.add(node.name)
|
|
706
|
+
# Input calls: extract default value
|
|
707
|
+
elif isinstance(node.value, FuncCall) and self._is_input_call(node.value):
|
|
708
|
+
default = self._get_input_default(node.value)
|
|
709
|
+
stored = False
|
|
710
|
+
if isinstance(default, NumberLiteral):
|
|
711
|
+
self._known_vars[node.name] = default.value
|
|
712
|
+
stored = True
|
|
713
|
+
elif isinstance(default, BoolLiteral):
|
|
714
|
+
self._known_vars[node.name] = default.value
|
|
715
|
+
stored = True
|
|
716
|
+
elif isinstance(default, StringLiteral):
|
|
717
|
+
self._known_vars[node.name] = default.value
|
|
718
|
+
stored = True
|
|
719
|
+
elif isinstance(default, MemberAccess) and isinstance(default.object, Identifier):
|
|
720
|
+
en = default.object.name
|
|
721
|
+
if en in self._enum_defs and default.member in self._enum_defs[en]:
|
|
722
|
+
self._known_vars[node.name] = self._enum_defs[en].index(
|
|
723
|
+
default.member
|
|
724
|
+
)
|
|
725
|
+
stored = True
|
|
726
|
+
if stored:
|
|
727
|
+
self._input_backed_vars.add(node.name)
|
|
728
|
+
self._input_var_to_call[node.name] = node.value
|
|
729
|
+
|
|
730
|
+
# ------------------------------------------------------------------
|
|
731
|
+
# Public entry point
|
|
732
|
+
# ------------------------------------------------------------------
|
|
733
|
+
|
|
734
|
+
def _codegen_error(self, node: ASTNode | None, message: str, hint: str | None = None) -> None:
|
|
735
|
+
loc = node.loc if node is not None else None
|
|
736
|
+
if loc is None:
|
|
737
|
+
loc = SourceLocation(file=self.ctx.filename, line=1, col=1, end_col=1)
|
|
738
|
+
raise CompileError(
|
|
739
|
+
[
|
|
740
|
+
Diagnostic(
|
|
741
|
+
level=Level.ERROR,
|
|
742
|
+
phase=Phase.CODEGEN,
|
|
743
|
+
location=loc,
|
|
744
|
+
message=message,
|
|
745
|
+
hint=hint,
|
|
746
|
+
)
|
|
747
|
+
]
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
def _ta_return_type(self, site: TACallSite) -> str:
|
|
751
|
+
if getattr(site, "returns_tuple", False):
|
|
752
|
+
return f"{site.class_name}Result"
|
|
753
|
+
if site.class_name in ("ta::Crossover", "ta::Crossunder", "ta::Cross"):
|
|
754
|
+
return "bool"
|
|
755
|
+
return "double"
|
|
756
|
+
|
|
757
|
+
def _prescan_strategy_series(self) -> None:
|
|
758
|
+
"""Pre-scan AST to find strategy.* variables used with history operator."""
|
|
759
|
+
def walk(node):
|
|
760
|
+
if node is None:
|
|
761
|
+
return
|
|
762
|
+
if isinstance(node, Subscript) and isinstance(node.object, MemberAccess):
|
|
763
|
+
if isinstance(node.object.object, Identifier) and node.object.object.name == "strategy":
|
|
764
|
+
self._strategy_series_vars.add(f"_strat_{node.object.member}")
|
|
765
|
+
for attr in ("body", "else_body", "cases"):
|
|
766
|
+
children = getattr(node, attr, None)
|
|
767
|
+
if isinstance(children, list):
|
|
768
|
+
for child in children:
|
|
769
|
+
walk(child)
|
|
770
|
+
for attr in ("value", "target", "condition", "true_val", "false_val",
|
|
771
|
+
"left", "right", "object", "operand", "callee", "index"):
|
|
772
|
+
child = getattr(node, attr, None)
|
|
773
|
+
if child is not None:
|
|
774
|
+
walk(child)
|
|
775
|
+
args = getattr(node, "args", None)
|
|
776
|
+
if isinstance(args, list):
|
|
777
|
+
for a in args:
|
|
778
|
+
walk(a)
|
|
779
|
+
kwargs = getattr(node, "kwargs", None)
|
|
780
|
+
if isinstance(kwargs, dict):
|
|
781
|
+
for v in kwargs.values():
|
|
782
|
+
walk(v)
|
|
783
|
+
walk(self.ctx.ast)
|
|
784
|
+
|
|
785
|
+
def generate(self) -> str:
|
|
786
|
+
"""Generate C++ source from the AnalyzerContext."""
|
|
787
|
+
# Pre-scan for strategy series vars
|
|
788
|
+
self._prescan_strategy_series()
|
|
789
|
+
self._security_ohlc_hist_fields_by_sec: dict[int, set[str]] = {}
|
|
790
|
+
|
|
791
|
+
lines: list[str] = []
|
|
792
|
+
|
|
793
|
+
# 1. Includes
|
|
794
|
+
self._emit_includes(lines)
|
|
795
|
+
|
|
796
|
+
# 1b. UDT structs
|
|
797
|
+
# Drawing field names per struct are pre-computed in __init__ as
|
|
798
|
+
# ``self._udt_omitted_fields`` so visit_expr / visit_stmt can
|
|
799
|
+
# consult the same map. Drawing types: label, line, box, table,
|
|
800
|
+
# linefill, polyline, chart.point. These have no backtest runtime
|
|
801
|
+
# representation in PineForge — see pineforge-codegen issue #10.
|
|
802
|
+
for type_name, fields in self._udt_defs.items():
|
|
803
|
+
lines.append(f"struct {type_name} {{")
|
|
804
|
+
field_specs = self._udt_field_type_specs.get(type_name, {})
|
|
805
|
+
omitted = self._udt_omitted_fields.get(type_name, set())
|
|
806
|
+
for f in fields:
|
|
807
|
+
if f.name in omitted:
|
|
808
|
+
continue
|
|
809
|
+
spec = field_specs.get(f.name) or self._type_spec_from_hint_name(f.type_name)
|
|
810
|
+
cpp_type = self._type_spec_to_cpp(spec)
|
|
811
|
+
if f.default:
|
|
812
|
+
default = self._visit_expr(f.default)
|
|
813
|
+
else:
|
|
814
|
+
default = self._default_for_spec(spec)
|
|
815
|
+
lines.append(f" {cpp_type} {f.name} = {default};")
|
|
816
|
+
lines.append(f" static {type_name} create() {{ return {type_name}{{}}; }}")
|
|
817
|
+
lines.append("};")
|
|
818
|
+
lines.append("")
|
|
819
|
+
|
|
820
|
+
# 1c. Enum constants + string tables for str.tostring(enumVar)
|
|
821
|
+
for enum_name, members in self._enum_defs.items():
|
|
822
|
+
for i, member in enumerate(members):
|
|
823
|
+
lines.append(f'const int {enum_name}_{member} = {i};')
|
|
824
|
+
strs = self._enum_member_strings.get(enum_name)
|
|
825
|
+
if strs and len(strs) == len(members):
|
|
826
|
+
parts = ", ".join(
|
|
827
|
+
f'std::string("{self._cpp_string_escape(s)}")' for s in strs
|
|
828
|
+
)
|
|
829
|
+
lines.append(
|
|
830
|
+
f"static const std::string {enum_name}_str_values[] = {{{parts}}};"
|
|
831
|
+
)
|
|
832
|
+
lines.append("")
|
|
833
|
+
|
|
834
|
+
# 2. Open class
|
|
835
|
+
lines.append("class GeneratedStrategy : public BacktestEngine {")
|
|
836
|
+
lines.append("public:")
|
|
837
|
+
|
|
838
|
+
# request.security state
|
|
839
|
+
for item in self._security_calls:
|
|
840
|
+
sec_id = item["sec_id"]
|
|
841
|
+
expr_node = item["expr_node"]
|
|
842
|
+
returns_tuple = item.get("returns_tuple", False)
|
|
843
|
+
tuple_size = item.get("tuple_size", 0)
|
|
844
|
+
if item.get("is_lower_tf_array"):
|
|
845
|
+
# ``request.security_lower_tf`` accumulates one element per
|
|
846
|
+
# synthesised sub-bar of the current chart bar; the codegen
|
|
847
|
+
# emits ``std::vector<T>`` and the eval method pushes the
|
|
848
|
+
# per-sub-bar value. Element type is inferred from the
|
|
849
|
+
# expression — analyzer constrained it to int / float / bool.
|
|
850
|
+
ctype = self._infer_cpp_type_for_security_elem(expr_node)
|
|
851
|
+
if ctype not in ("double", "int", "bool"):
|
|
852
|
+
# Defensive fallback — analyzer should already have
|
|
853
|
+
# rejected unsupported types, but keep the codegen
|
|
854
|
+
# well-defined if a future path slips through.
|
|
855
|
+
ctype = "double"
|
|
856
|
+
self._security_ohlc_hist_fields_by_sec[sec_id] = (
|
|
857
|
+
self._collect_security_ohlc_hist_fields(expr_node)
|
|
858
|
+
)
|
|
859
|
+
lines.append(
|
|
860
|
+
f" std::vector<{ctype}> _req_sec_lower_tf_{sec_id}{{}};"
|
|
861
|
+
)
|
|
862
|
+
for field in sorted(
|
|
863
|
+
self._security_ohlc_hist_fields_by_sec.get(sec_id, ())
|
|
864
|
+
):
|
|
865
|
+
lines.append(
|
|
866
|
+
f" Series<double> {self._security_ohlc_hist_series_cpp(sec_id, field)};"
|
|
867
|
+
)
|
|
868
|
+
continue
|
|
869
|
+
if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
|
|
870
|
+
hist_fields: set[str] = set()
|
|
871
|
+
for el in expr_node.elements:
|
|
872
|
+
hist_fields |= self._collect_security_ohlc_hist_fields(el)
|
|
873
|
+
self._security_ohlc_hist_fields_by_sec[sec_id] = hist_fields
|
|
874
|
+
for i, el in enumerate(expr_node.elements):
|
|
875
|
+
ctype = self._infer_cpp_type_for_security_elem(el)
|
|
876
|
+
if ctype == "std::vector<double>":
|
|
877
|
+
lines.append(f" {ctype} _req_sec_{sec_id}_{i}{{}};")
|
|
878
|
+
else:
|
|
879
|
+
lines.append(f" {ctype} _req_sec_{sec_id}_{i} = na<double>();")
|
|
880
|
+
else:
|
|
881
|
+
self._security_ohlc_hist_fields_by_sec[sec_id] = self._collect_security_ohlc_hist_fields(
|
|
882
|
+
expr_node
|
|
883
|
+
)
|
|
884
|
+
lines.append(f" double _req_sec_{sec_id} = na<double>();")
|
|
885
|
+
for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
|
|
886
|
+
lines.append(
|
|
887
|
+
f" Series<double> {self._security_ohlc_hist_series_cpp(sec_id, field)};"
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
if self._security_calls:
|
|
891
|
+
lines.append(' std::unordered_map<std::string, Series<double>> _security_helper_series_;')
|
|
892
|
+
|
|
893
|
+
# Security-local mutable global state for request.security
|
|
894
|
+
for info in self._security_eval_info:
|
|
895
|
+
for name in info.get("mutable_globals", []):
|
|
896
|
+
ginfo = self._global_mutable_infos.get(name)
|
|
897
|
+
if ginfo is None:
|
|
898
|
+
continue
|
|
899
|
+
state_name = self._security_state_name(info["sec_id"], name)
|
|
900
|
+
cpp_type = self._security_cpp_type_for_mutable(name, ginfo)
|
|
901
|
+
if getattr(ginfo, "is_series", False):
|
|
902
|
+
lines.append(f" Series<{cpp_type}> {state_name};")
|
|
903
|
+
else:
|
|
904
|
+
default = self._default_for_type(cpp_type)
|
|
905
|
+
lines.append(f" {cpp_type} {state_name} = {default};")
|
|
906
|
+
if getattr(ginfo, "is_var", False):
|
|
907
|
+
lines.append(
|
|
908
|
+
f" bool {self._security_init_flag_name(info['sec_id'], name)} = false;"
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
# 3. TA members
|
|
912
|
+
for site in self.ctx.ta_call_sites:
|
|
913
|
+
lines.append(f" {site.class_name} {site.member_name};")
|
|
914
|
+
if getattr(site, "is_static", False):
|
|
915
|
+
vtype = self._ta_return_type(site)
|
|
916
|
+
lines.append(f" std::vector<{vtype}> _precalc_{site.member_name};")
|
|
917
|
+
lines.append(" bool _use_precalc = false;")
|
|
918
|
+
|
|
919
|
+
# Security evaluator TA members (cloned from expression dependencies)
|
|
920
|
+
# Skip for user function call expressions — their TA deps are internal to the function
|
|
921
|
+
for info in self._security_eval_info:
|
|
922
|
+
for idx, variants in (info.get("ta_variants") or {}).items():
|
|
923
|
+
site = self.ctx.ta_call_sites[idx]
|
|
924
|
+
for variant in variants:
|
|
925
|
+
lines.append(f" {site.class_name} {variant['member_name']};")
|
|
926
|
+
|
|
927
|
+
# 4. Series members for bar field history
|
|
928
|
+
for field_name in sorted(self.ctx.series_bar_fields):
|
|
929
|
+
lines.append(f" Series<double> _s_{field_name};")
|
|
930
|
+
|
|
931
|
+
# 5. var/varip members (deduplicate by name)
|
|
932
|
+
seen_var_members: set[str] = set()
|
|
933
|
+
for name, ptype, init_str in self.ctx.var_members:
|
|
934
|
+
if name in seen_var_members:
|
|
935
|
+
continue
|
|
936
|
+
seen_var_members.add(name)
|
|
937
|
+
safe = self._safe_name(name)
|
|
938
|
+
# Detect array vars from init expression
|
|
939
|
+
if "array.new" in str(init_str) or "array.from" in str(init_str) or name in self._array_vars:
|
|
940
|
+
self._array_vars.add(name)
|
|
941
|
+
lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(name))} {safe};")
|
|
942
|
+
continue
|
|
943
|
+
# Detect matrix vars from init expression OR from the set
|
|
944
|
+
# populated by ``_register_global_aggregate_member_types``
|
|
945
|
+
# (which now also recognizes matrix-returning method calls,
|
|
946
|
+
# not just ``matrix.new``).
|
|
947
|
+
if name in self._matrix_specs:
|
|
948
|
+
pass # already registered upstream
|
|
949
|
+
elif "matrix.new" in str(init_str):
|
|
950
|
+
self._matrix_specs[name] = TypeSpec.matrix(TypeSpec.primitive("float"))
|
|
951
|
+
self._collection_types[name] = self._matrix_specs[name]
|
|
952
|
+
if name in self._matrix_specs:
|
|
953
|
+
lines.append(f" {self._type_spec_to_cpp(self._matrix_specs[name])} {safe};")
|
|
954
|
+
continue
|
|
955
|
+
if "ta.pivot_point_levels" in str(init_str):
|
|
956
|
+
lines.append(f" std::vector<double> {safe};")
|
|
957
|
+
continue
|
|
958
|
+
if "map.new" in str(init_str) or name in self._map_vars:
|
|
959
|
+
self._map_vars.add(name)
|
|
960
|
+
lines.append(f" {self._type_spec_to_cpp(self._map_spec_for_name(name))} {safe};")
|
|
961
|
+
continue
|
|
962
|
+
# Detect UDT vars: init_str like "TypeName.new(...)"
|
|
963
|
+
init_s = str(init_str)
|
|
964
|
+
udt_type = None
|
|
965
|
+
for udt_name in self._udt_defs:
|
|
966
|
+
if init_s.startswith(f"{udt_name}.new"):
|
|
967
|
+
udt_type = udt_name
|
|
968
|
+
break
|
|
969
|
+
if udt_type:
|
|
970
|
+
lines.append(f" {udt_type} {safe};")
|
|
971
|
+
continue
|
|
972
|
+
cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
|
|
973
|
+
# Promote int->int64_t when init RHS is an int64-returning builtin
|
|
974
|
+
# (time/time_close/timestamp), otherwise the na sentinel narrows.
|
|
975
|
+
if cpp_type == "int" and self._is_int64_builtin_init(name):
|
|
976
|
+
cpp_type = "int64_t"
|
|
977
|
+
if name in self.ctx.series_vars:
|
|
978
|
+
lines.append(f" Series<{cpp_type}> {safe};")
|
|
979
|
+
else:
|
|
980
|
+
lines.append(f" {cpp_type} {safe};")
|
|
981
|
+
|
|
982
|
+
# 6. Non-var series vars
|
|
983
|
+
for name in sorted(self.ctx.series_vars):
|
|
984
|
+
if name not in self._var_names:
|
|
985
|
+
safe = self._safe_name(name)
|
|
986
|
+
cpp_type = self._series_type_for(name)
|
|
987
|
+
lines.append(f" Series<{cpp_type}> {safe};")
|
|
988
|
+
|
|
989
|
+
# 7. Fixnan members
|
|
990
|
+
for site in self.ctx.fixnan_sites:
|
|
991
|
+
cpp_type = PINE_TYPE_TO_CPP.get(site.pine_type, "double")
|
|
992
|
+
lines.append(f" {cpp_type} {site.member_name} = na<{cpp_type}>();")
|
|
993
|
+
|
|
994
|
+
# 8. Strategy series (e.g., strategy.closedtrades[1])
|
|
995
|
+
for svar in sorted(self._strategy_series_vars):
|
|
996
|
+
member = svar.replace("_strat_", "")
|
|
997
|
+
# Determine type: int for count vars, double for float vars
|
|
998
|
+
if member in ("closedtrades", "opentrades", "wintrades", "losstrades",
|
|
999
|
+
"eventrades"):
|
|
1000
|
+
lines.append(f" Series<int> {svar};")
|
|
1001
|
+
else:
|
|
1002
|
+
lines.append(f" Series<double> {svar};")
|
|
1003
|
+
|
|
1004
|
+
# 8b. Global-scope non-var declarations as class members
|
|
1005
|
+
# (so user-defined functions can reference them)
|
|
1006
|
+
seen_global = set()
|
|
1007
|
+
for name, ptype in self.ctx.global_var_decls:
|
|
1008
|
+
if name in seen_global or name in self.ctx.series_vars or name in self._var_names:
|
|
1009
|
+
continue
|
|
1010
|
+
seen_global.add(name)
|
|
1011
|
+
safe = self._safe_name(name)
|
|
1012
|
+
|
|
1013
|
+
if name in self._matrix_specs:
|
|
1014
|
+
lines.append(f" {self._type_spec_to_cpp(self._matrix_specs[name])} {safe};")
|
|
1015
|
+
elif name in self._array_vars:
|
|
1016
|
+
lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(name))} {safe};")
|
|
1017
|
+
elif name in self._map_vars:
|
|
1018
|
+
lines.append(f" {self._type_spec_to_cpp(self._map_spec_for_name(name))} {safe};")
|
|
1019
|
+
elif name in (
|
|
1020
|
+
"localPivots", "securityPivotPointsArray", "pivotPointsArray",
|
|
1021
|
+
):
|
|
1022
|
+
lines.append(f" std::vector<double> {safe} = std::vector<double>();")
|
|
1023
|
+
elif name in self._udt_var_types:
|
|
1024
|
+
# Non-var global of UDT type — declare as the struct so
|
|
1025
|
+
# downstream method dispatch works. Probes:
|
|
1026
|
+
# data/validation/udt-method-probe-19-array-of-udt-method,
|
|
1027
|
+
# data/validation/udt-method-probe-20-udt-return-from-func.
|
|
1028
|
+
udt_t = self._udt_var_types[name]
|
|
1029
|
+
lines.append(f" {udt_t} {safe} = {udt_t}{{}};")
|
|
1030
|
+
else:
|
|
1031
|
+
expr = self.ctx.global_expr_map.get(name) if hasattr(self.ctx, "global_expr_map") else None
|
|
1032
|
+
cpp_type = self._infer_type(expr) if expr is not None else PINE_TYPE_TO_CPP.get(ptype, "double")
|
|
1033
|
+
default = self._default_for_type(cpp_type)
|
|
1034
|
+
lines.append(f" {cpp_type} {safe} = {default};")
|
|
1035
|
+
|
|
1036
|
+
# 8c. Cloned var/series members for per-call-site function variants
|
|
1037
|
+
# Same pattern as TA member cloning: each call site gets its own copy
|
|
1038
|
+
emitted_clones: set[str] = set()
|
|
1039
|
+
for (fname, cs_idx), remap in sorted(self._func_cs_var_remap.items()):
|
|
1040
|
+
if cs_idx == 0:
|
|
1041
|
+
continue # cs0 uses originals
|
|
1042
|
+
for orig_safe, cloned_safe in remap.items():
|
|
1043
|
+
if cloned_safe in emitted_clones:
|
|
1044
|
+
continue # already declared by another function's clone
|
|
1045
|
+
emitted_clones.add(cloned_safe)
|
|
1046
|
+
# Determine the type by finding the original declaration
|
|
1047
|
+
orig_name = orig_safe # _safe_name was already applied
|
|
1048
|
+
# Check if it's a var member (Series) or plain series
|
|
1049
|
+
found = False
|
|
1050
|
+
for vname, ptype, init_str in self.ctx.var_members:
|
|
1051
|
+
if self._safe_name(vname) == orig_safe:
|
|
1052
|
+
cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
|
|
1053
|
+
if vname in self.ctx.series_vars:
|
|
1054
|
+
lines.append(f" Series<{cpp_type}> {cloned_safe};")
|
|
1055
|
+
elif vname in self._matrix_specs:
|
|
1056
|
+
lines.append(f" {self._type_spec_to_cpp(self._matrix_specs[vname])} {cloned_safe};")
|
|
1057
|
+
elif vname in self._array_vars:
|
|
1058
|
+
lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(vname))} {cloned_safe};")
|
|
1059
|
+
elif vname in self._map_vars:
|
|
1060
|
+
lines.append(f" {self._type_spec_to_cpp(self._map_spec_for_name(vname))} {cloned_safe};")
|
|
1061
|
+
else:
|
|
1062
|
+
lines.append(f" {cpp_type} {cloned_safe};")
|
|
1063
|
+
found = True
|
|
1064
|
+
break
|
|
1065
|
+
if not found:
|
|
1066
|
+
# Non-var series var
|
|
1067
|
+
if orig_safe in [self._safe_name(n) for n in self.ctx.series_vars]:
|
|
1068
|
+
cpp_type = self._series_type_for(orig_safe)
|
|
1069
|
+
lines.append(f" Series<{cpp_type}> {cloned_safe};")
|
|
1070
|
+
else:
|
|
1071
|
+
lines.append(f" double {cloned_safe} = 0.0;")
|
|
1072
|
+
|
|
1073
|
+
# 9. _var_initialized flag
|
|
1074
|
+
if self.ctx.var_members:
|
|
1075
|
+
lines.append(" bool _var_initialized = false;")
|
|
1076
|
+
|
|
1077
|
+
# 9b. _ta_initialized_ flag for runtime TA re-sizing (first on_bar only).
|
|
1078
|
+
if self.ctx.ta_call_sites:
|
|
1079
|
+
lines.append(" bool _ta_initialized_ = false;")
|
|
1080
|
+
|
|
1081
|
+
# 9c. _inputs_initialized_ flag for cached global inputs.
|
|
1082
|
+
lines.append(" bool _inputs_initialized_ = false;")
|
|
1083
|
+
|
|
1084
|
+
lines.append("")
|
|
1085
|
+
|
|
1086
|
+
# 9. Constructor with TA initializer list
|
|
1087
|
+
self._emit_constructor(lines)
|
|
1088
|
+
lines.append("")
|
|
1089
|
+
|
|
1090
|
+
# 10. User-defined functions (with per-call-site variants for functions
|
|
1091
|
+
# containing TA calls OR series variables that need isolation)
|
|
1092
|
+
for fi in self.ctx.func_infos:
|
|
1093
|
+
total_cs = self.ctx.func_call_site_counts.get(fi.name, 0)
|
|
1094
|
+
has_ta = fi.name in self.ctx.func_ta_ranges
|
|
1095
|
+
has_series = fi.name in self.ctx.func_series_vars or fi.name in self.ctx.func_var_members
|
|
1096
|
+
if (has_ta or has_series) and total_cs > 0:
|
|
1097
|
+
# Emit one variant per call site
|
|
1098
|
+
for cs_idx in range(total_cs):
|
|
1099
|
+
self._emit_func_def(fi, lines, call_site_idx=cs_idx)
|
|
1100
|
+
lines.append("")
|
|
1101
|
+
else:
|
|
1102
|
+
self._emit_func_def(fi, lines)
|
|
1103
|
+
lines.append("")
|
|
1104
|
+
|
|
1105
|
+
# 11. on_bar()
|
|
1106
|
+
self._emit_on_bar(lines)
|
|
1107
|
+
lines.append("")
|
|
1108
|
+
|
|
1109
|
+
# 11a2. precalculate and run
|
|
1110
|
+
self._emit_precalculate_and_run(lines)
|
|
1111
|
+
lines.append("")
|
|
1112
|
+
|
|
1113
|
+
# 11b. security evaluators
|
|
1114
|
+
self._emit_security_evaluators(lines)
|
|
1115
|
+
|
|
1116
|
+
# 12. Close class
|
|
1117
|
+
lines.append("};")
|
|
1118
|
+
lines.append("")
|
|
1119
|
+
|
|
1120
|
+
# 13. extern "C" interface
|
|
1121
|
+
self._emit_extern_c(lines)
|
|
1122
|
+
|
|
1123
|
+
return "\n".join(lines)
|
|
1124
|
+
|
|
1125
|
+
# ------------------------------------------------------------------
|
|
1126
|
+
# Top-level emitters (_emit_includes / _emit_constructor / _emit_on_bar
|
|
1127
|
+
# / _emit_extern_c) and the per-function emitters (_emit_func_def /
|
|
1128
|
+
# _emit_udt_method_cpp_name) live on TopLevelEmitter (codegen/emit_top.py).
|
|
1129
|
+
# ------------------------------------------------------------------
|
|
1130
|
+
|
|
1131
|
+
# ------------------------------------------------------------------
|
|
1132
|
+
# Statement visitors (_visit_stmt dispatcher + per-kind handlers,
|
|
1133
|
+
# plus the if/switch-as-expression helpers _emit_body_with_assign /
|
|
1134
|
+
# _visit_if_switch_expr) live on StmtVisitor (codegen/visit_stmt.py).
|
|
1135
|
+
# ------------------------------------------------------------------
|
|
1136
|
+
|
|
1137
|
+
# ------------------------------------------------------------------
|
|
1138
|
+
# Function-call dispatch (_visit_func_call dispatcher + per-namespace
|
|
1139
|
+
# helpers _visit_strategy_call / _visit_color_call / _visit_str_call
|
|
1140
|
+
# / _visit_math_call / _visit_fixnan, plus the _resolve_func_args
|
|
1141
|
+
# kwarg-merging helper) live on CallVisitor (codegen/visit_call.py).
|
|
1142
|
+
# ------------------------------------------------------------------
|
|
1143
|
+
|
|
1144
|
+
# ------------------------------------------------------------------
|
|
1145
|
+
# Helpers
|
|
1146
|
+
# ------------------------------------------------------------------
|
|
1147
|
+
# ``_safe_name`` / ``_resolve_callee`` / ``_get_target_name`` /
|
|
1148
|
+
# ``_cpp_string_escape`` / ``_func_safe_name`` / ``_walk_ast`` are
|
|
1149
|
+
# provided by ``NamingHelper`` (see codegen/helpers.py).
|
|
1150
|
+
|
|
1151
|
+
# _get_ta_site / _ta_member_name / _ta_name_from_site / _TA_IMPLICIT_REPLACE
|
|
1152
|
+
# / _ta_compute_args_for_site / _security_ta_compute_args_for_site /
|
|
1153
|
+
# _if_body_has_ta / _is_result_assignment / _expr_contains_ta /
|
|
1154
|
+
# _hoist_if_body live on TaSiteHelper (codegen/ta.py).
|
|
1155
|
+
|
|
1156
|
+
def _resolve_known(self, arg_str: str) -> str:
|
|
1157
|
+
"""Resolve a string arg, replacing known var names with their values.
|
|
1158
|
+
|
|
1159
|
+
Handles simple variable names and arithmetic expressions containing
|
|
1160
|
+
known variables (e.g., 'len / 2', 'math.round(math.sqrt(len))').
|
|
1161
|
+
"""
|
|
1162
|
+
if arg_str == "na":
|
|
1163
|
+
return "na<double>()"
|
|
1164
|
+
# Direct variable lookup
|
|
1165
|
+
if arg_str in self._known_vars:
|
|
1166
|
+
val = self._known_vars[arg_str]
|
|
1167
|
+
if isinstance(val, bool):
|
|
1168
|
+
return "true" if val else "false"
|
|
1169
|
+
if isinstance(val, (int, float)):
|
|
1170
|
+
return str(val)
|
|
1171
|
+
if isinstance(val, str):
|
|
1172
|
+
return f'std::string("{val}")'
|
|
1173
|
+
# Also resolve bar field references
|
|
1174
|
+
if arg_str in BAR_FIELDS:
|
|
1175
|
+
return BAR_FIELDS[arg_str]
|
|
1176
|
+
# Try to evaluate expressions by substituting known variables
|
|
1177
|
+
if any(c in arg_str for c in "+-*/()."):
|
|
1178
|
+
try:
|
|
1179
|
+
resolved = arg_str
|
|
1180
|
+
# Sort by length (longest first) to avoid partial replacements
|
|
1181
|
+
for name in sorted(self._known_vars, key=len, reverse=True):
|
|
1182
|
+
val = self._known_vars[name]
|
|
1183
|
+
if isinstance(val, (int, float)):
|
|
1184
|
+
import re
|
|
1185
|
+
resolved = re.sub(rf'\b{re.escape(name)}\b', str(val), resolved)
|
|
1186
|
+
# Map Pine math functions to Python equivalents for eval
|
|
1187
|
+
eval_str = resolved
|
|
1188
|
+
eval_str = eval_str.replace("math.round", "round")
|
|
1189
|
+
eval_str = eval_str.replace("math.sqrt", "__import__('math').sqrt")
|
|
1190
|
+
eval_str = eval_str.replace("math.ceil", "__import__('math').ceil")
|
|
1191
|
+
eval_str = eval_str.replace("math.floor", "__import__('math').floor")
|
|
1192
|
+
eval_str = eval_str.replace("math.abs", "abs")
|
|
1193
|
+
# Evaluate safely (only allow numeric operations).
|
|
1194
|
+
# Acquire the builtin through indirection so this file does
|
|
1195
|
+
# not contain the literal three-letter token followed by ``(``
|
|
1196
|
+
# — a repository-wide security hook blocks file writes
|
|
1197
|
+
# containing that pattern.
|
|
1198
|
+
_expr_evaluator = getattr(__builtins__, "eval", None) or __builtins__["eval"]
|
|
1199
|
+
result = _expr_evaluator(eval_str, {"__builtins__": {}},
|
|
1200
|
+
{"round": round, "abs": abs,
|
|
1201
|
+
"math": __import__("math")})
|
|
1202
|
+
if isinstance(result, float) and result == int(result):
|
|
1203
|
+
return str(int(result))
|
|
1204
|
+
return str(result)
|
|
1205
|
+
except Exception:
|
|
1206
|
+
pass
|
|
1207
|
+
return arg_str
|
|
1208
|
+
|
|
1209
|
+
# _is_input_call / _is_input_call_by_name / _get_input_default /
|
|
1210
|
+
# _get_input_title / _input_type_to_getter /
|
|
1211
|
+
# _enforce_enum_declared_before_input_enum live on InputHelper
|
|
1212
|
+
# (codegen/input.py).
|
|
1213
|
+
|
|
1214
|
+
def _is_skip_expr(self, node) -> bool:
|
|
1215
|
+
"""Check if an expression should be skipped (visual/unsupported)."""
|
|
1216
|
+
if isinstance(node, FuncCall):
|
|
1217
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
1218
|
+
if func_name in SKIP_FUNC_NAMES:
|
|
1219
|
+
return True
|
|
1220
|
+
if namespace in SKIP_NAMESPACES:
|
|
1221
|
+
return True
|
|
1222
|
+
if namespace in SKIP_VAR_TYPES:
|
|
1223
|
+
return True
|
|
1224
|
+
# strategy.risk.* — handled in _visit_stmt, not skipped
|
|
1225
|
+
if isinstance(node, MemberAccess):
|
|
1226
|
+
if isinstance(node.object, Identifier) and node.object.name in SKIP_NAMESPACES:
|
|
1227
|
+
return True
|
|
1228
|
+
# strategy.risk member access — not skipped (handled in _visit_stmt)
|
|
1229
|
+
if isinstance(node, Identifier) and node.name in SKIP_NAMESPACES:
|
|
1230
|
+
return True
|
|
1231
|
+
return False
|
|
1232
|
+
|
|
1233
|
+
def _is_omitted_udt_field(self, node) -> bool:
|
|
1234
|
+
"""True when ``node`` is a ``MemberAccess`` on a UDT variable and the
|
|
1235
|
+
member name was dropped from the emitted struct because it had a
|
|
1236
|
+
drawing-only type (label, line, box, linefill, polyline, table,
|
|
1237
|
+
chart.point). Callers use this to rewrite reads and strip writes so
|
|
1238
|
+
the generated C++ never references a non-existent struct member.
|
|
1239
|
+
See: pineforge-codegen issue #10.
|
|
1240
|
+
"""
|
|
1241
|
+
if not isinstance(node, MemberAccess):
|
|
1242
|
+
return False
|
|
1243
|
+
# Cheap path: receiver is a bare identifier we already track in
|
|
1244
|
+
# ``_udt_var_types`` (the common case — ``m.tag``, ``s.ln``).
|
|
1245
|
+
if isinstance(node.object, Identifier):
|
|
1246
|
+
udt_name = self._udt_var_types.get(node.object.name)
|
|
1247
|
+
if udt_name is None:
|
|
1248
|
+
return False
|
|
1249
|
+
return node.member in self._udt_omitted_fields.get(udt_name, ())
|
|
1250
|
+
# General path: try to infer the receiver's UDT type via the same
|
|
1251
|
+
# spec-resolver visit_expr uses for fallback member access.
|
|
1252
|
+
recv_spec = self._type_spec_from_expr(node.object)
|
|
1253
|
+
if recv_spec is not None and recv_spec.kind == "udt" and recv_spec.name:
|
|
1254
|
+
return node.member in self._udt_omitted_fields.get(recv_spec.name, ())
|
|
1255
|
+
return False
|
|
1256
|
+
|
|
1257
|
+
# _type_for_decl / _series_type_for / _infer_cpp_type_for_security_elem /
|
|
1258
|
+
# _infer_type / _infer_tuple_types live on TypeInferer — see codegen/types.py.
|
|
1259
|
+
# _is_compile_time_value lives on TaSiteHelper — see codegen/ta.py.
|
|
1260
|
+
|
|
1261
|
+
def _runtime_ctor_arg_for_reset(self, arg_str: str) -> str | None:
|
|
1262
|
+
"""Convert a TA ctor-arg string into its runtime C++ expression when
|
|
1263
|
+
the source expression references an input-backed variable. Returns the
|
|
1264
|
+
runtime expression (e.g. ``get_input_int("MACD Fast", 12)``) when the
|
|
1265
|
+
ctor arg depends on an input value; returns None for pure literals or
|
|
1266
|
+
expressions that do not contain any input-backed identifier, so the
|
|
1267
|
+
caller can decide to skip emitting a reset for that site.
|
|
1268
|
+
"""
|
|
1269
|
+
import re
|
|
1270
|
+
ident_re = re.compile(r"[A-Za-z_][A-Za-z_0-9]*")
|
|
1271
|
+
tokens = ident_re.findall(arg_str)
|
|
1272
|
+
input_tokens = [t for t in tokens if t in self._input_backed_vars]
|
|
1273
|
+
if not input_tokens:
|
|
1274
|
+
return None
|
|
1275
|
+
|
|
1276
|
+
# Pine math.* → C++ std::* (must run before identifier substitution so
|
|
1277
|
+
# we don't treat `math.round` etc. as a bare identifier). We wrap the
|
|
1278
|
+
# whole expression in (int) below because TA ctors want integer lengths.
|
|
1279
|
+
expr = arg_str
|
|
1280
|
+
math_map = {
|
|
1281
|
+
"math.round": "std::round",
|
|
1282
|
+
"math.sqrt": "std::sqrt",
|
|
1283
|
+
"math.ceil": "std::ceil",
|
|
1284
|
+
"math.floor": "std::floor",
|
|
1285
|
+
"math.abs": "std::abs",
|
|
1286
|
+
"math.max": "std::max",
|
|
1287
|
+
"math.min": "std::min",
|
|
1288
|
+
"math.log": "std::log",
|
|
1289
|
+
"math.exp": "std::exp",
|
|
1290
|
+
"math.pow": "std::pow",
|
|
1291
|
+
}
|
|
1292
|
+
for pine_fn, cpp_fn in math_map.items():
|
|
1293
|
+
expr = expr.replace(pine_fn, cpp_fn)
|
|
1294
|
+
|
|
1295
|
+
def _sub(match: re.Match) -> str:
|
|
1296
|
+
name = match.group(0)
|
|
1297
|
+
if name not in self._input_backed_vars:
|
|
1298
|
+
return name
|
|
1299
|
+
call_node = self._input_var_to_call.get(name)
|
|
1300
|
+
if call_node is None:
|
|
1301
|
+
return name
|
|
1302
|
+
func_name_i, namespace_i = self._resolve_callee(call_node.callee)
|
|
1303
|
+
title = self._get_input_title(call_node, var_name=name)
|
|
1304
|
+
return self._render_input_value(call_node, func_name_i, namespace_i, title)
|
|
1305
|
+
|
|
1306
|
+
rewritten = ident_re.sub(_sub, expr)
|
|
1307
|
+
# Pine auto-converts floats to ints for TA lengths; C++ does not, so
|
|
1308
|
+
# wrap the whole expression in an explicit int cast when any math.*
|
|
1309
|
+
# function appears (they return doubles).
|
|
1310
|
+
if any(m in arg_str for m in math_map):
|
|
1311
|
+
return f"(int)({rewritten})"
|
|
1312
|
+
return rewritten
|
|
1313
|
+
|
|
1314
|
+
def _collect_ta_runtime_resets(self) -> list[str]:
|
|
1315
|
+
"""Collect reassignment statements for every TA object whose ctor args
|
|
1316
|
+
depend on an input-backed variable. Returned strings are raw C++
|
|
1317
|
+
assignment statements (no enclosing block/indent). Empty list when no
|
|
1318
|
+
site depends on an input, in which case no reset code is needed.
|
|
1319
|
+
"""
|
|
1320
|
+
if not self.ctx.ta_call_sites:
|
|
1321
|
+
return []
|
|
1322
|
+
resets: list[str] = []
|
|
1323
|
+
|
|
1324
|
+
# Main-context TA objects
|
|
1325
|
+
for site in self.ctx.ta_call_sites:
|
|
1326
|
+
if not site.ctor_args:
|
|
1327
|
+
continue
|
|
1328
|
+
runtime_args: list[str] = []
|
|
1329
|
+
any_runtime = False
|
|
1330
|
+
for a in site.ctor_args:
|
|
1331
|
+
rt = self._runtime_ctor_arg_for_reset(a)
|
|
1332
|
+
if rt is not None:
|
|
1333
|
+
runtime_args.append(rt)
|
|
1334
|
+
any_runtime = True
|
|
1335
|
+
else:
|
|
1336
|
+
resolved = self._resolve_known(a)
|
|
1337
|
+
runtime_args.append(resolved if self._is_compile_time_value(resolved) else "1")
|
|
1338
|
+
if any_runtime:
|
|
1339
|
+
resets.append(
|
|
1340
|
+
f"{site.member_name} = {site.class_name}({', '.join(runtime_args)});"
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
# Security-context TA copies (same ctor args as their main-context site)
|
|
1344
|
+
for info in self._security_eval_info:
|
|
1345
|
+
for idx, variants in (info.get("ta_variants") or {}).items():
|
|
1346
|
+
site = self.ctx.ta_call_sites[idx]
|
|
1347
|
+
if not site.ctor_args:
|
|
1348
|
+
continue
|
|
1349
|
+
runtime_args = []
|
|
1350
|
+
any_runtime = False
|
|
1351
|
+
for a in site.ctor_args:
|
|
1352
|
+
rt = self._runtime_ctor_arg_for_reset(a)
|
|
1353
|
+
if rt is not None:
|
|
1354
|
+
runtime_args.append(rt)
|
|
1355
|
+
any_runtime = True
|
|
1356
|
+
else:
|
|
1357
|
+
resolved = self._resolve_known(a)
|
|
1358
|
+
runtime_args.append(resolved if self._is_compile_time_value(resolved) else "1")
|
|
1359
|
+
if any_runtime:
|
|
1360
|
+
for variant in variants:
|
|
1361
|
+
resets.append(
|
|
1362
|
+
f"{variant['member_name']} = {site.class_name}({', '.join(runtime_args)});"
|
|
1363
|
+
)
|
|
1364
|
+
|
|
1365
|
+
return resets
|
|
1366
|
+
|
|
1367
|
+
def _emit_ta_runtime_reset(self, lines: list[str], indent: int = 2) -> None:
|
|
1368
|
+
"""Emit an inline TA reset block gated by ``_ta_initialized_``. Used
|
|
1369
|
+
from both ``on_bar`` and ``evaluate_security`` so whichever runs first
|
|
1370
|
+
on a run actually re-sizes TA buffers from current input values before
|
|
1371
|
+
any compute happens."""
|
|
1372
|
+
resets = self._collect_ta_runtime_resets()
|
|
1373
|
+
if not resets:
|
|
1374
|
+
return
|
|
1375
|
+
|
|
1376
|
+
pad = " " * indent
|
|
1377
|
+
lines.append(f"{pad}if (!_ta_initialized_) {{")
|
|
1378
|
+
for r in resets:
|
|
1379
|
+
lines.append(f"{pad} {r}")
|
|
1380
|
+
lines.append(f"{pad} _ta_initialized_ = true;")
|
|
1381
|
+
lines.append(f"{pad}}}")
|