@pineforge/codegen-pyodide 0.7.0

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.
Files changed (37) hide show
  1. package/README.md +25 -0
  2. package/index.mjs +20 -0
  3. package/package.json +32 -0
  4. package/pineforge_codegen/__init__.py +53 -0
  5. package/pineforge_codegen/analyzer/__init__.py +60 -0
  6. package/pineforge_codegen/analyzer/base.py +1566 -0
  7. package/pineforge_codegen/analyzer/call_handlers.py +895 -0
  8. package/pineforge_codegen/analyzer/contracts.py +163 -0
  9. package/pineforge_codegen/analyzer/diagnostics.py +118 -0
  10. package/pineforge_codegen/analyzer/tables.py +204 -0
  11. package/pineforge_codegen/analyzer/types.py +261 -0
  12. package/pineforge_codegen/ast_nodes.py +293 -0
  13. package/pineforge_codegen/codegen/__init__.py +78 -0
  14. package/pineforge_codegen/codegen/base.py +1381 -0
  15. package/pineforge_codegen/codegen/emit_top.py +875 -0
  16. package/pineforge_codegen/codegen/helpers.py +163 -0
  17. package/pineforge_codegen/codegen/helpers_syminfo.py +132 -0
  18. package/pineforge_codegen/codegen/input.py +189 -0
  19. package/pineforge_codegen/codegen/security.py +1564 -0
  20. package/pineforge_codegen/codegen/ta.py +298 -0
  21. package/pineforge_codegen/codegen/tables.py +683 -0
  22. package/pineforge_codegen/codegen/types.py +592 -0
  23. package/pineforge_codegen/codegen/visit_call.py +1387 -0
  24. package/pineforge_codegen/codegen/visit_expr.py +729 -0
  25. package/pineforge_codegen/codegen/visit_stmt.py +766 -0
  26. package/pineforge_codegen/errors.py +98 -0
  27. package/pineforge_codegen/lexer.py +531 -0
  28. package/pineforge_codegen/parser.py +1198 -0
  29. package/pineforge_codegen/pragmas.py +117 -0
  30. package/pineforge_codegen/signatures.py +808 -0
  31. package/pineforge_codegen/support_checker.py +1223 -0
  32. package/pineforge_codegen/symbols.py +118 -0
  33. package/pineforge_codegen/tokens.py +406 -0
  34. package/pineforge_codegen/tv_input_choices.py +86 -0
  35. package/pineforge_codegen-0.7.0.tar.gz +0 -0
  36. package/release.json +7 -0
  37. package/tables.json +1653 -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}}}")