@pineforge/codegen-pyodide 0.8.0 → 0.9.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.
@@ -39,6 +39,7 @@ from .tables import (
39
39
  BAR_FIELDS,
40
40
  BAR_BUILTINS,
41
41
  BAR_SERIES_PUSH,
42
+ DRAWING_TYPE_TO_CPP,
42
43
  SECURITY_OHLC_BAR_FIELDS,
43
44
  TA_RETURNS_BOOL,
44
45
  TA_IMPLICIT_COMPUTE,
@@ -52,6 +53,7 @@ from .tables import (
52
53
  SKIP_VAR_TYPES,
53
54
  SYMINFO_MEMBER_MAP,
54
55
  COLOR_CONST_MAP,
56
+ ARRAY_NEW_CTORS,
55
57
  ARRAY_METHODS,
56
58
  MAP_METHODS,
57
59
  MATRIX_METHODS,
@@ -64,6 +66,15 @@ from .tables import (
64
66
  _merge_kwargs,
65
67
  )
66
68
 
69
+ TA_TUPLE_RESULT_TYPES = {
70
+ "macd": "ta::MACDResult",
71
+ "supertrend": "ta::SupertrendResult",
72
+ "dmi": "ta::DMIResult",
73
+ "bb": "ta::BBResult",
74
+ "kc": "ta::KCResult",
75
+ "vwap_bands": "ta::VWAPBandsResult",
76
+ }
77
+
67
78
  # (TA_IMPLICIT_COMPUTE / TA_COMPUTE_ARGS now imported from .tables above.)
68
79
 
69
80
  # (TA_IMPLICIT_COMPUTE_FULL / TA_IMPLICIT_APPEND / PINE_TYPE_TO_CPP /
@@ -112,12 +123,17 @@ from .visit_stmt import StmtVisitor
112
123
  from .visit_expr import ExprVisitor
113
124
  from .visit_call import CallVisitor
114
125
 
126
+ # DrawingVisitor owns the drawing-objects-as-data dispatch (line/box/label/
127
+ # linefill/chart.point lowering onto the per-type arenas) plus _uses_drawing
128
+ # detection and arena-cap computation. See codegen/drawing.py.
129
+ from .drawing import DrawingVisitor
130
+
115
131
 
116
132
  # ---------------------------------------------------------------------------
117
133
  # CodeGen class
118
134
  # ---------------------------------------------------------------------------
119
135
 
120
- class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, NamingHelper):
136
+ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, DrawingVisitor, NamingHelper):
121
137
  """Generate C++ from an AnalyzerContext (visitor pattern).
122
138
 
123
139
  Mixin chain (Python MRO is left-to-right; method names are
@@ -175,9 +191,41 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
175
191
  self._func_cs_var_remap: dict[tuple[str, int], dict[str, str]] = {}
176
192
  # Active var name remap (set during per-call-site function emission)
177
193
  self._active_var_remap: dict[str, str] = {}
194
+ # When True (only while lowering a TA runtime-reset length expression
195
+ # through the expression visitor), an input-backed variable identifier
196
+ # renders as an override-aware ``get_input_*()`` read instead of its
197
+ # class member name. The reset can run in ``evaluate_security`` BEFORE
198
+ # the input members are initialised, so it must not depend on their
199
+ # init order. See ``_lower_reset_expr_via_visitor``.
200
+ self._reset_input_getter_mode: bool = False
178
201
  # Set of var/series member names that belong to user functions (need cloning)
179
202
  self._func_var_members_set: set[str] = set()
203
+ # BUG C: function-local names emitted as ``UDT*`` pointer aliases (a UDT
204
+ # local initialised from a var/global UDT lvalue, mutated through, AND
205
+ # later rebound to a different lvalue). Member access lowers to ``->``
206
+ # and rebinds to ``&(...)``. Reset per function in _emit_func_def is not
207
+ # needed: names are function-unique and the value-copy fallback ignores
208
+ # entries for inactive functions.
209
+ self._udt_ptr_alias_locals: set[str] = set()
210
+ # Names of hoisted GLOBAL-scope UDT loop-locals bound from a UDT array
211
+ # element (``z = arr.get(i)``) and later field-mutated. Pine array
212
+ # elements of a user-defined type are references, so such a local must
213
+ # ALIAS the element, not value-copy — the mutation has to write back
214
+ # into the array. These are de-hoisted from the class-member value-copy
215
+ # to a fresh per-iteration ``UDT& z = arr[i];`` local reference (the same
216
+ # form the non-hoisted function-local alias path already emits). Read-only
217
+ # get-locals are NOT recorded (no field mutation) and keep value-copy
218
+ # semantics. Populated by _register_udt_array_get_ref_locals.
219
+ self._udt_array_get_ref_locals: set[str] = set()
180
220
  self._precalc_loop_active: bool = False
221
+ # Names of ``var`` members that live in a FUNCTION scope (not global).
222
+ # These are initialized once-per-function-variant on first call (a
223
+ # function-local static equivalent), NOT in the constructor / on_bar
224
+ # preamble. See ``_emit_func_var_init_block``.
225
+ self._func_local_var_names: set[str] = set()
226
+ for _vlist in ctx.func_var_members.values():
227
+ for _n, _, _ in _vlist:
228
+ self._func_local_var_names.add(_n)
181
229
 
182
230
  # Build per-function var/series name lists for cloning.
183
231
  # For each function with call-site variants, collect ALL function-scoped
@@ -260,30 +308,91 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
260
308
 
261
309
  # Build cloned site remapping for cs > 0 (must happen before _ta_site_map
262
310
  # so cloned names are in _func_ta_members and get filtered out of the map)
311
+ #
312
+ # Default to the ``{orig}_cs{cs_idx}`` formula (matches the analyzer's clone
313
+ # naming), but defer to the analyzer's authoritative clone-name map for any
314
+ # site it had to disambiguate (a TA site reached through multiple enclosing
315
+ # functions would otherwise collide on the formula). Keeping the formula as
316
+ # the default leaves all non-colliding output byte-identical.
317
+ clone_names = getattr(ctx, "func_cs_ta_clone_names", {})
263
318
  for fname, orig_names in func_ta_originals.items():
264
319
  total_cs = ctx.func_call_site_counts.get(fname, 1)
265
320
  for cs_idx in range(1, total_cs):
321
+ overrides = clone_names.get((fname, cs_idx), {})
266
322
  remap = {}
267
323
  for orig_name in orig_names:
268
- remap[orig_name] = f"{orig_name}_cs{cs_idx}"
324
+ remap[orig_name] = overrides.get(
325
+ orig_name, f"{orig_name}_cs{cs_idx}")
269
326
  self._func_cs_ta_remap[(fname, cs_idx)] = remap
270
327
  self._func_ta_members.update(remap.values())
271
328
 
272
329
  for site in ctx.ta_call_sites:
273
330
  if site.node is not None:
274
331
  if site.member_name not in self._func_ta_members:
332
+ # Top-level (non-function) site: maps to itself.
275
333
  self._ta_site_map[id(site.node)] = site
276
- elif not any(site.member_name.endswith(f"_cs{i}") for i in range(1, 100)):
277
- # Original (cs0) function-local site — add to map for initial visit
334
+ elif id(site.node) not in self._ta_site_map:
335
+ # Function-local site. Multiple clones share the SAME AST
336
+ # node (clones copy ``node=orig.node``); the FIRST one in
337
+ # ``ta_call_sites`` order is the canonical original (cs0)
338
+ # whose ``member_name`` the per-call-site remap is keyed on.
339
+ # Later clones (``_cs{i}``, ``_cs{i}_cs{j}``, ``_u{n}`` …)
340
+ # must NOT overwrite it: doing so poisons the base name so
341
+ # the active-remap lookup misses and every clone collapses
342
+ # onto one member. Keep the original; the variant member is
343
+ # resolved via ``_active_ta_remap`` at emit time.
278
344
  self._ta_site_map[id(site.node)] = site
279
345
  self._ta_index_by_site_id: dict[int, int] = {
280
346
  id(site): i for i, site in enumerate(ctx.ta_call_sites)
281
347
  }
348
+ # Context-sensitive (call-path) instance machinery for nested stateful
349
+ # helpers. Built by ``_build_func_instances`` below. ``_current_instance_name``
350
+ # names the function clone whose body is currently being emitted (None at
351
+ # top level / non-variant bodies). ``_instance_dispatch`` maps
352
+ # ``(enclosing_instance_name, call_node_id) -> callee emit-name`` and is the
353
+ # authority for nested stateful-helper dispatch (see visit_call).
354
+ self._current_instance_name: str | None = None
355
+ self._instance_dispatch: dict[tuple[str | None, int], str] = {}
356
+ self._fresh_instances: list[dict] = []
357
+ self._fresh_var_members: list[tuple[str, str]] = []
358
+ # Fresh fixnan members for context-sensitive helper instances (nested
359
+ # helpers reached through >1 distinct call path). Each fresh instance
360
+ # gets its OWN previous-value member so two paths never share fixnan
361
+ # state. Populated by ``_build_func_instances``; declared in step 7.
362
+ self._fresh_fixnan_members: list[tuple[Any, str]] = []
363
+ # NOTE: _build_func_instances() runs at the top of generate() (it needs
364
+ # _all_member_names / _func_safe_name, which are populated later in __init__).
282
365
  # Build lookup: node id -> FixnanCallSite (counter-based)
283
366
  self._fixnan_counter = 0
367
+ # Per-call-site fixnan member remap for user functions (mirrors the TA
368
+ # remap): (func_name, cs_idx) -> {orig_member: cloned_member}.
369
+ self._func_cs_fixnan_remap: dict[tuple[str, int], dict[str, str]] = {}
370
+ # Active fixnan remap (set during per-call-site function emission).
371
+ self._active_fixnan_remap: dict[str, str] = {}
372
+ # node id -> original FixnanCallSite (the cs0 / source-level site).
373
+ self._fixnan_site_map: dict[int, Any] = {}
374
+ # Set of fixnan member names that belong to user functions (excluded
375
+ # from the site map so the active remap can dispatch per variant).
376
+ self._func_fixnan_members: set[str] = set()
377
+ # Dead fixnan site indices (owner is a dead user function). Skipped
378
+ # at declaration time so dead functions' fixnan state is not emitted.
379
+ self._dead_fixnan_indices: set[int] = set()
284
380
  self._switch_counter = 0
285
381
  self._security_inline_counter = 0
286
382
  self._random_call_counter = 0
383
+ self._for_counter = 0
384
+ # Synthetic history buffers used by inline call-history and by scalar
385
+ # expressions passed to UDF series parameters. They are pre-registered
386
+ # at generate() time so declarations precede method emission, then
387
+ # addressed by (source node, emitted UDF variant). Each record is a
388
+ # real class-member Series and therefore joins _PFScriptState through
389
+ # the declaration-derived checkpoint inventory.
390
+ self._inline_history_members: list[dict] = []
391
+ self._inline_history_member_by_key: dict[tuple, str] = {}
392
+ # Unique lambda-local names used when an array lowering references its
393
+ # receiver more than once. The binding keeps temporary-producing or
394
+ # side-effectful receivers single-evaluation (see TypeInferer).
395
+ self._array_receiver_counter = 0
287
396
  # UDT / enum (needed before _collect_known_vars for input.enum)
288
397
  self._udt_defs: dict[str, list] = {}
289
398
  self._enum_defs: dict[str, list[str]] = {}
@@ -308,10 +417,32 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
308
417
  # Map input-backed var name -> its input.*() FuncCall node so we can
309
418
  # later emit a runtime get_input_*() read with the same title/default.
310
419
  self._input_var_to_call: dict[str, FuncCall] = {}
420
+ # Class-scope arithmetic-over-input vars (e.g. ``wilderLen = rsiLen*2-1``).
421
+ # Maps the derived var name -> its raw RHS expression string. When such a
422
+ # var feeds a TA ctor length, the runtime-reset path expands it so input
423
+ # overrides propagate (``(get_input_int("RSI Length",14) * 2 - 1)``); the
424
+ # ctor-init list still folds to the Pine-default literal via _resolve_known.
425
+ self._derived_input_expr: dict[str, str] = {}
311
426
  self._timeframe_period_vars: set[str] = set()
427
+ # Names of class-scope vars whose value is a bar-invariant scalar —
428
+ # i.e. derived only from inputs, literals, ``timeframe.*`` members,
429
+ # ``math.*`` over stable args, and ternaries/casts/arithmetic over
430
+ # any of those. Such vars are safe to embed in a TA ctor runtime
431
+ # reset expression (they do not depend on per-bar series). Vars
432
+ # referencing series / ta.* results / history subscripts / strategy
433
+ # state are NOT here, so a TA length fed by them is still rejected
434
+ # by the constructor guard.
435
+ self._stable_runtime_vars: set[str] = set()
436
+ # ``_var_names`` (var/varip persistent-state members) is needed by the
437
+ # stability classifier during _collect_known_vars, so pre-seed it from
438
+ # the analyzer's var_members before that pass runs; the canonical
439
+ # assignment below preserves the existing initialization order.
440
+ self._var_names: set[str] = set()
441
+ for _vn, _, _ in ctx.var_members:
442
+ self._var_names.add(_vn)
312
443
  self._collect_known_vars()
313
444
  # Track var names
314
- self._var_names: set[str] = set()
445
+ self._var_names = set()
315
446
  for name, _, _ in ctx.var_members:
316
447
  self._var_names.add(name)
317
448
  # Every name bound ANYWHERE in the program (top-level, nested in
@@ -328,6 +459,87 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
328
459
  for fi in ctx.func_infos:
329
460
  self._func_names.add(fi.name)
330
461
  self._func_info_map[fi.name] = fi
462
+ # Dead-code user functions: those that contain TA call sites but are
463
+ # never called anywhere in the script (no call site registered them,
464
+ # so func_call_site_counts reports 0). Their OWN TA ctor args still
465
+ # carry bare parameter names (e.g. ``dirmov_short(len) => ta.rma(ta.tr, len)``)
466
+ # that can never be resolved to a concrete length, and since the
467
+ # function never runs its TA buffers would be dead weight anyway.
468
+ # Track the dead TA site indices and dead function names so emission
469
+ # can skip both — the ctor guard no longer hard-fails on the bare
470
+ # param and no dangling member/function body is emitted. A function
471
+ # with zero call sites but NO TA state is NOT dead-by-this-rule (it
472
+ # may still be emitted; harmless if truly unreferenced).
473
+ #
474
+ # IMPORTANT: dead-ness of a TA site is decided by the site's
475
+ # ``owner_func`` (set by the analyzer), NOT by which function's
476
+ # ``func_ta_ranges`` slice the site happens to fall in. A function's
477
+ # slice can include clones of ANOTHER (live) function's sites that
478
+ # were minted while visiting THIS function's body (a nested call to
479
+ # a live callee registers the callee's cs{N} clones in the caller's
480
+ # TA-range slice). Keying dead-ness off the slice would drop those
481
+ # borrowed clones' declarations, leaving the owning callee's emitted
482
+ # clone body referencing undeclared members. Regression:
483
+ # quantbyboji-nq-hma-midday (``_ta_change_*_cs1`` / ``_ta_rma_*_cs1``
484
+ # minted inside dead ``adx_short``'s body but owned by live ``dirmov``).
485
+ self._dead_func_names: set[str] = set()
486
+ self._dead_ta_indices: set[int] = set()
487
+ for _fn in (ctx.func_ta_ranges or {}):
488
+ if (ctx.func_call_site_counts or {}).get(_fn, 0) > 0:
489
+ continue
490
+ # Only treat plain user functions (not UDT methods) as skippable
491
+ # dead code; methods are dispatched through the UDT and their
492
+ # call-site tracking is handled separately.
493
+ fi = self._func_info_map.get(_fn)
494
+ if fi is not None and getattr(fi, "is_udt_method", False):
495
+ continue
496
+ self._dead_func_names.add(_fn)
497
+ # Mark TA sites dead ONLY when their owner is a dead function. A
498
+ # site with ``owner_func=None`` (top-level) or whose owner is a
499
+ # live function survives -- even if it sits inside a dead
500
+ # function's TA-range slice (it's a borrowed clone).
501
+ for _i, _site in enumerate(ctx.ta_call_sites):
502
+ _owner = getattr(_site, "owner_func", None)
503
+ if _owner is not None and _owner in self._dead_func_names:
504
+ self._dead_ta_indices.add(_i)
505
+ # Build per-call-site fixnan remap + site map (mirrors TA remap above).
506
+ # Dead fixnan sites (owner is a dead function) are skipped at decl time.
507
+ clone_fn_names = getattr(ctx, "func_cs_fixnan_clone_names", {})
508
+ for _i, _fsite in enumerate(ctx.fixnan_sites):
509
+ _fowner = getattr(_fsite, "owner_func", None)
510
+ if _fowner is not None and _fowner in self._dead_func_names:
511
+ self._dead_fixnan_indices.add(_i)
512
+ # cs0 fixnan remap is identity (originals). Build originals per func.
513
+ func_fixnan_originals: dict[str, list[str]] = {}
514
+ for _fname, _idxs in (ctx.func_fixnan_indices or {}).items():
515
+ origs = [ctx.fixnan_sites[i].member_name for i in _idxs
516
+ if i not in self._dead_fixnan_indices]
517
+ if origs:
518
+ func_fixnan_originals[_fname] = origs
519
+ self._func_cs_fixnan_remap[(_fname, 0)] = {m: m for m in origs}
520
+ self._func_fixnan_members.update(origs)
521
+ # cs > 0 remap uses the ``{orig}_cs{cs_idx}`` formula (or the
522
+ # analyzer's disambiguated name from func_cs_fixnan_clone_names).
523
+ for _fname, _origs in func_fixnan_originals.items():
524
+ _total_cs = ctx.func_call_site_counts.get(_fname, 1)
525
+ for _cs_idx in range(1, _total_cs):
526
+ _overrides = clone_fn_names.get((_fname, _cs_idx), {})
527
+ _remap = {}
528
+ for _orig in _origs:
529
+ _remap[_orig] = _overrides.get(_orig, f"{_orig}_cs{_cs_idx}")
530
+ self._func_cs_fixnan_remap[(_fname, _cs_idx)] = _remap
531
+ self._func_fixnan_members.update(_remap.values())
532
+ # Site map: node id -> original site (cs0). Skip dead sites and
533
+ # function-local originals (the active remap dispatches variants).
534
+ for _i, _fsite in enumerate(ctx.fixnan_sites):
535
+ if _i in self._dead_fixnan_indices:
536
+ continue
537
+ if _fsite.node is None:
538
+ continue
539
+ if _fsite.member_name not in self._func_fixnan_members:
540
+ self._fixnan_site_map[id(_fsite.node)] = _fsite
541
+ elif id(_fsite.node) not in self._fixnan_site_map:
542
+ self._fixnan_site_map[id(_fsite.node)] = _fsite
331
543
  # Track strategy series vars (e.g., strategy.closedtrades[1])
332
544
  self._strategy_series_vars: set[str] = set()
333
545
  # Track global-scope non-var declarations (emitted as class members)
@@ -337,6 +549,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
337
549
  self._global_mutable_infos: dict[str, object] = getattr(ctx, "global_mutable_infos", {}) or {}
338
550
  self._udt_var_types: dict[str, str] = getattr(ctx, "udt_var_types", {}) or {}
339
551
  self._collection_types: dict[str, TypeSpec] = getattr(ctx, "collection_types", {}) or {}
552
+ # id(block_node) -> {raw_var_name: unique_member} for block-scoped var
553
+ # name collisions (see Analyzer._visit_VarDecl). Activated into
554
+ # ``_active_var_remap`` while emitting the owning block's statements.
555
+ self._block_var_renames: dict[int, dict[str, str]] = getattr(ctx, "block_var_renames", {}) or {}
340
556
  self._udt_field_type_specs: dict[str, dict[str, TypeSpec]] = getattr(ctx, "udt_field_type_specs", {}) or {}
341
557
  # Map UDT struct name -> set of field names that were dropped from the
342
558
  # emitted C++ struct because they had drawing-only types (label, line,
@@ -347,7 +563,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
347
563
  # rewrite or strip downstream references to those fields so the
348
564
  # generated C++ never references a member that doesn't exist on the
349
565
  # emitted struct. See: pineforge-codegen issue #10.
350
- _DRAWING_TYPES_INIT = {"label", "line", "box", "table", "linefill", "polyline", "chart.point"}
566
+ # Drawing-objects-as-data: line/box/label/linefill/chart.point are now
567
+ # REAL data (un-dropped from UDT structs). Only table/polyline stay
568
+ # dropped (no C++ representation). See drawing-objects-as-data.md §4.2.
569
+ _DRAWING_TYPES_INIT = {"table", "polyline"}
351
570
  self._udt_omitted_fields: dict[str, set[str]] = {}
352
571
  for _type_name, _fields in self._udt_defs.items():
353
572
  _omitted = set()
@@ -359,12 +578,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
359
578
  self._security_calls: list[dict] = [self._normalize_security_call(item) for item in ctx.security_calls]
360
579
  # Current function parameter types (set during _emit_func_def)
361
580
  self._current_func_param_types: dict[str, str] = {}
581
+ self._current_func_param_specs: dict[str, "TypeSpec"] = {}
362
582
  # Current function params that are series (const Series<double>&)
363
583
  self._current_func_series_params: set[str] = set()
364
584
  # Locals declared in the function currently being emitted (symbol table loses them after analysis)
365
585
  self._current_func_locals: set[str] = set()
586
+ self._current_func_local_types: dict[str, str] = {}
366
587
  # for-in loop iterator names (must resolve member access, not enum fallback)
367
588
  self._current_loop_vars: set[str] = set()
589
+ self._current_loop_var_specs: dict[str, "TypeSpec"] = {}
368
590
  # Track array variables for codegen
369
591
  self._array_vars: set[str] = set()
370
592
  # Track map variables for codegen
@@ -378,6 +600,29 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
378
600
  self._map_vars.add(_name)
379
601
  elif _spec.kind == "udt" and _spec.name:
380
602
  self._udt_var_types.setdefault(_name, _spec.name)
603
+ # Table / polyline variables and params have NO C++ representation
604
+ # (SKIP_VAR_TYPES). A *method* call on such a receiver
605
+ # (``panel.cell(...)``, ``dash.merge_cells(...)``) is a visual no-op
606
+ # that must be dropped — but unlike the namespace form
607
+ # (``table.cell(...)``) the receiver is a bare var/param the
608
+ # namespace-based skip cannot see. Collect those names so
609
+ # ``_is_skip_expr`` can drop their method calls.
610
+ _SKIP_DECL_TYPES = set(SKIP_VAR_TYPES) | {"polyline"}
611
+ self._visual_drop_vars: set[str] = set()
612
+ for _node in self._walk_ast(self.ctx.ast):
613
+ if isinstance(_node, VarDecl):
614
+ if _node.type_hint in _SKIP_DECL_TYPES:
615
+ self._visual_drop_vars.add(_node.name)
616
+ elif isinstance(_node.value, FuncCall):
617
+ _fn, _ns = self._resolve_callee(_node.value.callee)
618
+ if _fn == "new" and _ns in _SKIP_DECL_TYPES:
619
+ self._visual_drop_vars.add(_node.name)
620
+ elif isinstance(_node, (FuncDef, MethodDef)):
621
+ _hints = (getattr(_node, "annotations", None) or {}).get("param_type_hints") or []
622
+ for _i, _p in enumerate(getattr(_node, "params", []) or []):
623
+ _h = _hints[_i] if _i < len(_hints) else None
624
+ if _h and str(_h).replace(" ", "") in _SKIP_DECL_TYPES:
625
+ self._visual_drop_vars.add(_p)
381
626
  # Collect request.security metadata per call
382
627
  self._security_eval_info: list[dict] = []
383
628
  self._security_ta_variant_names: dict[tuple[int, int, tuple], str] = {}
@@ -388,15 +633,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
388
633
  lookahead_node = item.get("lookahead_node")
389
634
  ta_range = item.get("ta_range")
390
635
 
391
- tf_str = None
392
- if isinstance(tf_node, StringLiteral):
393
- tf_str = tf_node.value
394
- elif (isinstance(tf_node, Identifier)
395
- and tf_node.name in self._known_vars
396
- and tf_node.name not in self._input_backed_vars):
397
- val = self._known_vars[tf_node.name]
398
- if isinstance(val, str):
399
- tf_str = val
636
+ # Resolve the timeframe: a literal/const/global gives a static tf;
637
+ # a function-parameter tf is resolved from the call sites (the
638
+ # evaluator is a class method, so the param is not in scope there).
639
+ tf_str, tf_expr = self._resolve_security_tf(
640
+ tf_node, item.get("containing_func", ""))
400
641
 
401
642
  is_lookahead_on = False
402
643
  if lookahead_node is not None:
@@ -447,9 +688,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
447
688
  self._security_eval_info.append({
448
689
  "sec_id": sec_id,
449
690
  "tf": tf_str,
691
+ "tf_expr": tf_expr,
450
692
  "tf_node": tf_node,
451
693
  "gaps_on": is_gaps_on,
452
694
  "lookahead_on": is_lookahead_on,
695
+ "heikinashi": bool(item.get("heikinashi", False)),
453
696
  "ta_range": ta_range,
454
697
  "ta_indices": sorted(ta_indices),
455
698
  "ta_binding_stacks": ta_binding_stacks,
@@ -467,7 +710,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
467
710
  self._all_member_names.add(self._safe_name(name))
468
711
 
469
712
  self._register_global_aggregate_member_types()
713
+ self._register_udt_array_get_ref_locals()
470
714
  self._uses_matrix = self._detect_matrix_usage()
715
+ # Drawing-objects-as-data: gate all new emission (drawing.hpp include +
716
+ # the per-type arenas) on this flag so non-drawing strategies stay
717
+ # byte-identical. Caps come from the strategy() header max_*_count.
718
+ self._uses_drawing = self._detect_drawing_usage()
719
+ self._drawing_caps = self._compute_drawing_caps() if self._uses_drawing else {}
471
720
 
472
721
  # max_bars_back: the per-variable history depth the engine's Series<T>
473
722
  # ring buffer should retain. Pine exposes this two ways — the
@@ -493,6 +742,225 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
493
742
  # history reads off security-helper series.
494
743
  self._max_bars_back_cap: int | None = self._compute_max_bars_back_cap()
495
744
 
745
+ # ------------------------------------------------------------------
746
+ # Context-sensitive (call-path) instance machinery
747
+ # ------------------------------------------------------------------
748
+ def _iter_func_calls(self, root) -> list:
749
+ """Collect every ``FuncCall`` node reachable from ``root`` (a stmt list
750
+ or single AST node). Order-independent; used by the instance pre-pass to
751
+ find nested user-function calls inside a function body."""
752
+ out: list = []
753
+ seen: set[int] = set()
754
+ stack: list = list(root) if isinstance(root, (list, tuple)) else [root]
755
+ while stack:
756
+ node = stack.pop()
757
+ if node is None:
758
+ continue
759
+ if isinstance(node, (list, tuple)):
760
+ stack.extend(node)
761
+ continue
762
+ if isinstance(node, dict):
763
+ stack.extend(node.values())
764
+ continue
765
+ if not hasattr(node, "__dict__"):
766
+ continue
767
+ nid = id(node)
768
+ if nid in seen:
769
+ continue
770
+ seen.add(nid)
771
+ if isinstance(node, FuncCall):
772
+ out.append(node)
773
+ for v in vars(node).values():
774
+ if isinstance(v, (list, tuple, dict)) or hasattr(v, "__dict__"):
775
+ stack.append(v)
776
+ return out
777
+
778
+ def _build_func_instances(self) -> None:
779
+ """Context-sensitive cloning of nested stateful helper functions.
780
+
781
+ A stateful helper ``G`` (carrying TA state and/or ``var`` members) may be
782
+ reached through several distinct call paths — e.g. ``leg`` called from
783
+ three clones of ``f_get`` (lengths 10/20/30) *and* directly. Each path is
784
+ a logically-distinct instance that must drive its OWN TA/var members.
785
+
786
+ The analyzer already mints the per-path members (via range-widening), but
787
+ the flat ``{G}_cs{idx}`` clone namespace conflates a callee's own textual
788
+ call sites with the enclosing function's call sites. This pre-pass walks
789
+ the call graph from each natural clone and, for every nested stateful
790
+ call, composes the enclosing clone's active remap with the callee's
791
+ per-call-site remap:
792
+
793
+ composed_ta[m] = R_enclosing.get(R_callee_cs[m], R_callee_cs[m])
794
+
795
+ When the composition equals the callee's natural ``cs{j}`` remap the call
796
+ dispatches to the existing ``{G}_cs{j}`` clone (output stays byte-identical
797
+ for the common single-caller case). Otherwise a fresh instance is minted,
798
+ bound to the path-specific members (and FRESH ``var`` members so two paths
799
+ never share scalar state). ``_instance_dispatch`` records the resolved
800
+ emit-name per ``(enclosing_instance, call_node)``; ``_fresh_instances`` /
801
+ ``_fresh_var_members`` carry the extra code to emit.
802
+ """
803
+ ctx = self.ctx
804
+ stateful = (set(ctx.func_ta_ranges.keys())
805
+ | set(ctx.func_var_members.keys())
806
+ | set(ctx.func_series_vars.keys())
807
+ | set(ctx.func_fixnan_indices.keys())
808
+ | set(ctx.func_security_clone_only))
809
+ if not stateful:
810
+ return
811
+
812
+ func_bodies: dict[str, list] = {}
813
+ for fi in ctx.func_infos:
814
+ node = getattr(fi, "node", None)
815
+ if node is not None and getattr(node, "body", None):
816
+ func_bodies.setdefault(fi.name, node.body)
817
+
818
+ def ta_originals(fname: str) -> list[str]:
819
+ return list(self._func_cs_ta_remap.get((fname, 0), {}).keys())
820
+
821
+ def var_originals(fname: str) -> list[str]:
822
+ return [self._safe_name(n) for n, _, _ in ctx.func_var_members.get(fname, [])]
823
+
824
+ def fixnan_originals(fname: str) -> list[str]:
825
+ return list(self._func_cs_fixnan_remap.get((fname, 0), {}).keys())
826
+
827
+ def natural_name(fname: str, cs_idx: int) -> str:
828
+ return f"{self._func_cpp_base_name(fname)}_cs{cs_idx}"
829
+
830
+ interned: dict[tuple, dict] = {}
831
+ worklist: list[dict] = []
832
+ seen_walk: set[str] = set()
833
+ fresh_counter = 0
834
+
835
+ # Seed with the natural clones the flat emission loop produces.
836
+ for fname in sorted(stateful):
837
+ if fname not in func_bodies:
838
+ continue
839
+ total_cs = ctx.func_call_site_counts.get(fname, 0)
840
+ if total_cs > 0:
841
+ for k in range(total_cs):
842
+ worklist.append({
843
+ "fname": fname,
844
+ "name": natural_name(fname, k),
845
+ "ta_remap": self._func_cs_ta_remap.get((fname, k), {}),
846
+ "var_remap": self._func_cs_var_remap.get((fname, k), {}),
847
+ "fixnan_remap": self._func_cs_fixnan_remap.get((fname, k), {}),
848
+ })
849
+ else:
850
+ worklist.append({
851
+ "fname": fname,
852
+ "name": self._func_cpp_base_name(fname),
853
+ "ta_remap": {},
854
+ "var_remap": {},
855
+ "fixnan_remap": {},
856
+ })
857
+
858
+ while worklist:
859
+ inst = worklist.pop()
860
+ if inst["name"] in seen_walk:
861
+ continue
862
+ seen_walk.add(inst["name"])
863
+ body = func_bodies.get(inst["fname"])
864
+ if not body:
865
+ continue
866
+ active_ta = inst["ta_remap"]
867
+ active_fixnan = inst.get("fixnan_remap", {})
868
+ for callnode in self._iter_func_calls(body):
869
+ cs_info = ctx.func_call_cs_map.get(id(callnode))
870
+ if cs_info is None:
871
+ continue
872
+ g_name, j = cs_info
873
+ if g_name not in stateful:
874
+ continue
875
+ natural_ta = self._func_cs_ta_remap.get((g_name, j), {})
876
+ composed_ta = {}
877
+ for m in ta_originals(g_name):
878
+ mid = natural_ta.get(m, m)
879
+ composed_ta[m] = active_ta.get(mid, mid)
880
+ natural_fixnan = self._func_cs_fixnan_remap.get((g_name, j), {})
881
+ composed_fixnan = {}
882
+ for m in fixnan_originals(g_name):
883
+ mid = natural_fixnan.get(m, m)
884
+ composed_fixnan[m] = active_fixnan.get(mid, mid)
885
+ if composed_ta == natural_ta and composed_fixnan == natural_fixnan:
886
+ # Path resolves to the callee's own cs{j} clone — reuse it.
887
+ self._instance_dispatch[(inst["name"], id(callnode))] = \
888
+ natural_name(g_name, j)
889
+ continue
890
+ key = (g_name, frozenset(composed_ta.items()),
891
+ frozenset(composed_fixnan.items()))
892
+ ginst = interned.get(key)
893
+ if ginst is None:
894
+ fresh_counter += 1
895
+ inst_name = f"{self._func_cpp_base_name(g_name)}__ni{fresh_counter}"
896
+ fvar_remap: dict[str, str] = {}
897
+ for v in var_originals(g_name):
898
+ fresh_member = f"{v}__ni{fresh_counter}"
899
+ fvar_remap[v] = fresh_member
900
+ self._fresh_var_members.append((v, fresh_member))
901
+ # Fresh fixnan members: each path gets its OWN previous-
902
+ # value member so two call paths never share fixnan state.
903
+ ffixnan_remap: dict[str, str] = {}
904
+ for orig_fn_member in fixnan_originals(g_name):
905
+ fresh_fn_member = f"{orig_fn_member}__ni{fresh_counter}"
906
+ ffixnan_remap[orig_fn_member] = fresh_fn_member
907
+ # Find the original FixnanCallSite to carry its type.
908
+ orig_fn_site = None
909
+ for _fs in ctx.fixnan_sites:
910
+ if _fs.member_name == orig_fn_member:
911
+ orig_fn_site = _fs
912
+ break
913
+ if orig_fn_site is not None:
914
+ self._fresh_fixnan_members.append(
915
+ (orig_fn_site, fresh_fn_member)
916
+ )
917
+ ginst = {
918
+ "fname": g_name,
919
+ "name": inst_name,
920
+ "ta_remap": composed_ta,
921
+ "var_remap": fvar_remap,
922
+ "fixnan_remap": ffixnan_remap,
923
+ }
924
+ interned[key] = ginst
925
+ self._fresh_instances.append(ginst)
926
+ worklist.append(ginst)
927
+ self._instance_dispatch[(inst["name"], id(callnode))] = ginst["name"]
928
+
929
+ def _emit_cloned_var_decl(self, orig_safe: str, cloned_safe: str,
930
+ series_suffix: str, lines: list[str]) -> None:
931
+ """Declare a per-clone copy of a function-scoped ``var`` member, matching
932
+ the original's C++ type (series / matrix / array / map / drawing-handle /
933
+ UDT / scalar). Shared by the per-call-site clone loop and the fresh
934
+ context-sensitive instance loop."""
935
+ for vname, ptype, _init_str in self.ctx.var_members:
936
+ if self._safe_name(vname) == orig_safe:
937
+ cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
938
+ if vname in self.ctx.series_vars:
939
+ lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
940
+ elif vname in self._matrix_specs:
941
+ lines.append(f" {self._type_spec_to_cpp(self._matrix_specs[vname])} {cloned_safe};")
942
+ elif vname in self._array_vars:
943
+ lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(vname))} {cloned_safe};")
944
+ elif vname in self._map_vars:
945
+ lines.append(f" {self._type_spec_to_cpp(self._map_spec_for_name(vname))} {cloned_safe};")
946
+ elif vname in self._udt_var_types:
947
+ # Drawing handle / UDT var clone must match the original's
948
+ # type (Line/Label/Box/<UDT>), not the coarse PineType
949
+ # default (double) — otherwise the clone can't hold the
950
+ # handle and drawing access on it reads a garbage / na id.
951
+ udt_t = self._udt_var_types[vname]
952
+ handle_cpp = DRAWING_TYPE_TO_CPP.get(udt_t, udt_t)
953
+ lines.append(f" {handle_cpp} {cloned_safe} = {handle_cpp}{{}};")
954
+ else:
955
+ lines.append(f" {cpp_type} {cloned_safe};")
956
+ return
957
+ # Non-var series var
958
+ if orig_safe in [self._safe_name(n) for n in self.ctx.series_vars]:
959
+ cpp_type = self._series_type_for(orig_safe)
960
+ lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
961
+ else:
962
+ lines.append(f" double {cloned_safe} = 0.0;")
963
+
496
964
  @staticmethod
497
965
  def _int_literal_value(node: ASTNode | None) -> int | None:
498
966
  """Return the integer value of a (possibly unary-minus) NumberLiteral,
@@ -575,31 +1043,33 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
575
1043
  spec = self._matrix_specs.get(recv_name) or TypeSpec.matrix(TypeSpec.primitive("float"))
576
1044
  self._matrix_specs[name] = spec
577
1045
  self._collection_types[name] = spec
578
- elif ns == "array" and fn in (
579
- "new",
580
- "new_float",
581
- "new_int",
582
- "new_bool",
583
- "new_string",
584
- "from",
585
- ):
1046
+ elif ns == "array" and fn in ({"new", "from"} | set(ARRAY_NEW_CTORS)):
586
1047
  self._array_vars.add(name)
1048
+ spec = self._type_spec_from_expr(expr) or self._array_spec_for_name(name)
1049
+ self._collection_types[name] = spec
587
1050
  elif ns == "map" and fn == "new":
588
1051
  self._map_vars.add(name)
589
1052
 
590
- # Also register var/varip matrix members from AST nodes so that
591
- # the typed-matrix gate checks see the correct element spec.
1053
+ # Also register var/varip aggregate members from AST nodes so that
1054
+ # class-member declarations see the precise collection type before
1055
+ # on_bar emits the initializer. This is required for unannotated
1056
+ # drawing arrays such as ``var boxes = array.new_box()``.
592
1057
  var_decl_map: dict[str, FuncCall] = {}
593
1058
  for stmt in (self.ctx.ast.body if hasattr(self.ctx, "ast") else []):
594
1059
  if isinstance(stmt, VarDecl) and isinstance(stmt.value, FuncCall):
595
1060
  var_decl_map[stmt.name] = stmt.value
596
1061
  for name, _ptype, _init_str in self.ctx.var_members:
597
- if name in self._matrix_specs:
598
- continue
599
1062
  expr = var_decl_map.get(name)
600
1063
  if expr is None:
601
1064
  continue
602
1065
  fn2, ns2 = self._resolve_callee(expr.callee)
1066
+ if ns2 == "array" and fn2 in ({"new", "from"} | set(ARRAY_NEW_CTORS)):
1067
+ self._array_vars.add(name)
1068
+ spec2 = self._type_spec_from_expr(expr) or self._array_spec_for_name(name)
1069
+ self._collection_types[name] = spec2
1070
+ continue
1071
+ if name in self._matrix_specs:
1072
+ continue
603
1073
  if ns2 == "matrix" and fn2 == "new":
604
1074
  targs2 = self._template_args_from_call(expr) if hasattr(expr, "annotations") else []
605
1075
  elem_spec2 = self._type_spec_from_hint_name(targs2[0]) if targs2 else TypeSpec.primitive("float")
@@ -622,6 +1092,68 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
622
1092
  self._matrix_specs[name] = spec2
623
1093
  self._collection_types[name] = spec2
624
1094
 
1095
+ def _walk_global_scope_with_loopflag(self, stmts, in_loop):
1096
+ """Yield ``(stmt, in_loop)`` for every statement in global scope,
1097
+ recursing into control-flow bodies (if/for/while/switch) but NOT into
1098
+ nested function definitions — a function-local of the same name lives in
1099
+ a separate scope and must not be attributed to a global member. The
1100
+ ``in_loop`` flag is True once inside any for/while loop body."""
1101
+ for s in stmts:
1102
+ if isinstance(s, FuncDef):
1103
+ continue
1104
+ yield s, in_loop
1105
+ child_in_loop = in_loop or isinstance(s, (ForStmt, ForInStmt, WhileStmt))
1106
+ for attr in ("body", "else_body", "default_body"):
1107
+ child = getattr(s, attr, None)
1108
+ if isinstance(child, list):
1109
+ yield from self._walk_global_scope_with_loopflag(child, child_in_loop)
1110
+ cases = getattr(s, "cases", None)
1111
+ if isinstance(cases, list):
1112
+ for _case_expr, case_stmts in cases:
1113
+ if isinstance(case_stmts, list):
1114
+ yield from self._walk_global_scope_with_loopflag(case_stmts, child_in_loop)
1115
+
1116
+ def _register_udt_array_get_ref_locals(self) -> None:
1117
+ """Detect global-scope UDT loop-locals that alias a UDT array element and
1118
+ are later field-mutated (Pine array elements of a user-defined type are
1119
+ references — ``z = arr.get(i)`` then ``z.f := v`` MUST write back into
1120
+ ``arr``).
1121
+
1122
+ A non-``var`` global-scope ``UDT z = arr.get(i)`` (or ``.first`` /
1123
+ ``.last``) nested inside a for/while loop mis-lowers to a value copy: a
1124
+ global ``while`` loop hoists ``z`` to a class member whose in-loop init
1125
+ becomes a value-copy assignment, and a global ``for`` loop keeps ``z`` a
1126
+ true local but the function-local alias path (``_udt_local_alias_kind``)
1127
+ no-ops at global scope (``_current_func_body`` is None) — both silently
1128
+ drop the field mutation. We record exactly this shape so the (possible)
1129
+ class member is suppressed and the in-loop VarDecl is emitted as a fresh
1130
+ per-iteration ``UDT& z = arr[i];`` reference instead (the same alias form
1131
+ the non-hoisted function-local path already produces). Strictly gated:
1132
+ the RHS must be a UDT-array-element lvalue AND the name must be field-
1133
+ mutated at global scope AND the declaration must be loop-nested. A
1134
+ read-only get-local is never recorded, so its value-copy output is
1135
+ unchanged. Function-local get-locals are excluded (the walker skips
1136
+ function bodies) — those keep using the existing alias path."""
1137
+ pairs = list(self._walk_global_scope_with_loopflag(self.ctx.ast.body, False))
1138
+ field_mutated: set[str] = set()
1139
+ for s, _in_loop in pairs:
1140
+ if (isinstance(s, Assignment)
1141
+ and isinstance(s.target, MemberAccess)
1142
+ and isinstance(s.target.object, Identifier)):
1143
+ field_mutated.add(s.target.object.name)
1144
+ for s, in_loop in pairs:
1145
+ if not isinstance(s, VarDecl) or s.is_var or s.is_varip:
1146
+ continue
1147
+ if not in_loop:
1148
+ continue
1149
+ if not isinstance(s.value, FuncCall):
1150
+ continue
1151
+ if self._is_udt_lvalue(s.value) is None:
1152
+ continue
1153
+ if s.name not in field_mutated:
1154
+ continue
1155
+ self._udt_array_get_ref_locals.add(s.name)
1156
+
625
1157
  def _extract_receiver_name(self, call_node) -> str | None:
626
1158
  """Extract receiver Identifier name from m.method(...) or matrix.method(m, ...).
627
1159
 
@@ -733,6 +1265,134 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
733
1265
  for stmt in self.ctx.ast.body:
734
1266
  if isinstance(stmt, VarDecl) and stmt.name not in reassigned:
735
1267
  self._collect_known_var(stmt)
1268
+ # A second pass handles the stable-reassigned-scalar pattern: a
1269
+ # class-scope scalar initialized from a stable expr and reassigned
1270
+ # ONLY inside top-level if/elif chains whose conditions and assigned
1271
+ # values are themselves stable (inputs / timeframe.* / math.*). Such
1272
+ # a var is a bar-invariant scalar and may feed a TA ctor length with
1273
+ # a runtime reset that reproduces the conditional logic. Series-
1274
+ # dependent reassignments are left untracked (rejected by the guard).
1275
+ self._collect_reassigned_stable_scalars(reassigned)
1276
+
1277
+ def _collect_reassigned_stable_scalars(self, reassigned: set[str]) -> None:
1278
+ """Track class-scope scalars that are reassigned but only along stable
1279
+ if/elif paths (see ``test_stable_reassigned_class_scope_length``).
1280
+
1281
+ For each top-level ``v = <init>`` whose name is reassigned, build the
1282
+ final value as a nested ternary by folding subsequent top-level
1283
+ IfStmts / direct Assignments. If every condition and every assigned
1284
+ RHS is stable (and renderable), record the ternary in
1285
+ ``_derived_input_expr`` and add ``v`` to ``_stable_runtime_vars`` so
1286
+ the TA ctor reset path can expand it. Anything non-stable (a ta.*
1287
+ result, a bar field, a series var) leaves the var untracked, so the
1288
+ ctor guard still rejects it loudly.
1289
+ """
1290
+ from ..ast_nodes import IfStmt, Assignment
1291
+ body = self.ctx.ast.body or []
1292
+ # Pre-resolve each reassigned var's initial VarDecl.
1293
+ inits: dict[str, object] = {}
1294
+ for stmt in body:
1295
+ if (isinstance(stmt, VarDecl) and stmt.name in reassigned
1296
+ and not stmt.is_var and not stmt.is_varip):
1297
+ # Only consider vars whose initial value is itself stable;
1298
+ # an unstable init cannot become a stable scalar via later
1299
+ # reassignment.
1300
+ if stmt.value is not None and self._expr_is_stable(stmt.value):
1301
+ inits[stmt.name] = stmt.value
1302
+ if not inits:
1303
+ return
1304
+
1305
+ def _value_after(stmts, fallback: str | None) -> str | None:
1306
+ """Fold a statement list into the final value expression for the
1307
+ target var, given ``fallback`` as the value on entry. Returns None
1308
+ if any condition / assignment is non-stable or unrenderable."""
1309
+ current = fallback
1310
+ for s in stmts or []:
1311
+ if isinstance(s, Assignment) and isinstance(s.target, Identifier):
1312
+ if s.target.name != target_name:
1313
+ continue
1314
+ if s.op != ":=":
1315
+ return None # compound assignment — not a stable fold
1316
+ if not self._expr_is_stable(s.value):
1317
+ return None
1318
+ rhs = self._arith_expr_to_str(s.value)
1319
+ if rhs is None:
1320
+ return None
1321
+ current = rhs
1322
+ elif isinstance(s, IfStmt):
1323
+ # Only model IfStmts that actually reassign the target var;
1324
+ # an unrelated IfStmt (e.g. entry/exit logic with a series
1325
+ # condition) must NOT abort the fold — the var simply keeps
1326
+ # its current value through it.
1327
+ if not _reassigns(s, target_name):
1328
+ continue
1329
+ if not self._expr_is_stable(s.condition):
1330
+ return None
1331
+ cond = self._arith_expr_to_str(s.condition)
1332
+ if cond is None:
1333
+ return None
1334
+ then_val = _value_after(s.body, current)
1335
+ if then_val is None:
1336
+ return None
1337
+ else_val = _value_after(s.else_body, current)
1338
+ if else_val is None:
1339
+ return None
1340
+ current = f"({cond} ? {then_val} : {else_val})"
1341
+ # Other statement shapes (for/while/switch/var decls of
1342
+ # other vars) are ignored for this var's value fold; they
1343
+ # do not reassign ``target_name`` in a way we model.
1344
+ return current
1345
+
1346
+ def _reassigns(node, name: str) -> bool:
1347
+ """True if any ``:=`` assignment to ``name`` occurs within node."""
1348
+ from ..ast_nodes import IfStmt as _If, Assignment as _Asg
1349
+ if isinstance(node, _Asg) and isinstance(node.target, Identifier):
1350
+ return node.target.name == name
1351
+ if isinstance(node, _If):
1352
+ if any(_reassigns(c, name) for c in (node.body or [])):
1353
+ return True
1354
+ if any(_reassigns(c, name) for c in (node.else_body or [])):
1355
+ return True
1356
+ return False
1357
+ for attr in ("body", "else_body", "cases", "default_body"):
1358
+ sub = getattr(node, attr, None)
1359
+ if isinstance(sub, list):
1360
+ if any(_reassigns(c, name) for c in sub):
1361
+ return True
1362
+ return False
1363
+
1364
+ for target_name, init_node in inits.items():
1365
+ init_str = self._arith_expr_to_str(init_node)
1366
+ if init_str is None:
1367
+ continue
1368
+ final = _value_after(body, init_str)
1369
+ if final is None:
1370
+ continue
1371
+ # Sanity: the fold must actually differ from the bare init,
1372
+ # otherwise there were no stable reassignments and the var is
1373
+ # already covered (or rejected) by the main pass.
1374
+ if final == init_str:
1375
+ continue
1376
+ # Fold to a compile-time literal when possible (so the ctor-init
1377
+ # list can use it directly); otherwise record the raw expression
1378
+ # for the runtime reset path to expand.
1379
+ folded = self._resolve_known(final)
1380
+ if self._is_compile_time_value(folded):
1381
+ try:
1382
+ num = float(folded)
1383
+ self._known_vars[target_name] = (
1384
+ int(num) if num == int(num) else num
1385
+ )
1386
+ except ValueError:
1387
+ pass
1388
+ self._derived_input_expr[target_name] = final
1389
+ self._stable_runtime_vars.add(target_name)
1390
+ # Mark input-backed iff the expression references an input so the
1391
+ # override-aware get_input_*() reads are emitted on the reset path.
1392
+ import re as _re
1393
+ toks = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", final))
1394
+ if any(t in self._input_backed_vars for t in toks):
1395
+ self._input_backed_vars.add(target_name)
736
1396
 
737
1397
  def _find_reassigned_vars(self) -> set[str]:
738
1398
  """Scan AST to find all variable names that are targets of := or compound assignment."""
@@ -759,6 +1419,180 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
759
1419
  walk(stmt)
760
1420
  return reassigned
761
1421
 
1422
+ # ``math.*`` members that are pure functions over their (stable) args, or
1423
+ # stable constants. Anything outside this set (e.g. ``math.random``) is
1424
+ # treated as non-stable. Used by ``_expr_is_stable``.
1425
+ _MATH_STABLE_MEMBERS: frozenset[str] = frozenset({
1426
+ "pi", "e", "phi", "rphi",
1427
+ "abs", "max", "min", "round", "floor", "ceil",
1428
+ "sqrt", "log", "log10", "exp", "pow",
1429
+ "sin", "cos", "tan", "asin", "acos", "atan", "sign",
1430
+ "sum", "avg", "to_precision", "round_to_mintick",
1431
+ })
1432
+
1433
+ # ``timeframe.*`` members that are constant for the lifetime of a run —
1434
+ # they reflect the script's resolution, not a per-bar value.
1435
+ _TF_STABLE_MEMBERS: frozenset[str] = frozenset({
1436
+ "period", "main_period", "multiplier",
1437
+ "isintraday", "isminutes", "isdaily", "isweekly",
1438
+ "ismonthly", "isdwm", "isseconds", "in_seconds", "isticks",
1439
+ })
1440
+
1441
+ def _expr_is_stable(self, node) -> bool:
1442
+ """True iff ``node``'s value is a bar-invariant scalar.
1443
+
1444
+ A stable expression depends only on: literals, ``input.*`` values,
1445
+ previously-tracked stable runtime vars, known compile-time consts,
1446
+ ``timeframe.*`` members (constant per run), ``syminfo.*`` (constant
1447
+ per instrument), and ``math.*`` functions/consts over stable
1448
+ sub-expressions, combined with arithmetic / comparison / logical
1449
+ ops, ternaries, and the ``int/float/bool/string`` casts.
1450
+
1451
+ Returns False (i.e. "series") for any node that references a per-bar
1452
+ value: bar fields (close/open/...), series vars, history subscripts,
1453
+ ``ta.*`` results, strategy.* state, or any unrecognised construct.
1454
+ The conservative False keeps the TA-ctor guard intact for genuinely
1455
+ dynamic lengths.
1456
+ """
1457
+ if node is None:
1458
+ return False
1459
+ if isinstance(node, (NumberLiteral, StringLiteral, BoolLiteral)):
1460
+ return True
1461
+ if isinstance(node, NaLiteral):
1462
+ return True
1463
+ if isinstance(node, Identifier):
1464
+ name = node.name
1465
+ if name in self._known_vars:
1466
+ return True
1467
+ if name in self._stable_runtime_vars:
1468
+ return True
1469
+ if name in self._input_backed_vars:
1470
+ return True
1471
+ if name in self.ctx.series_vars:
1472
+ return False
1473
+ if name in self._var_names:
1474
+ # var/varip persistent state — mutable across bars.
1475
+ return False
1476
+ if name in BAR_FIELDS or name in BAR_BUILTINS:
1477
+ return False
1478
+ # Unrecognised bare identifier: be conservative so we never
1479
+ # silently allow an undeclared / dynamic length through.
1480
+ return False
1481
+ if isinstance(node, MemberAccess):
1482
+ if isinstance(node.object, Identifier):
1483
+ ns = node.object.name
1484
+ if ns == "timeframe":
1485
+ return node.member in self._TF_STABLE_MEMBERS
1486
+ if ns == "math":
1487
+ return node.member in self._MATH_STABLE_MEMBERS
1488
+ if ns == "syminfo":
1489
+ # syminfo.* (mintick, pointvalue, tickerid, ...) is
1490
+ # constant for the run — safe as a stable scalar.
1491
+ return True
1492
+ # bar.* / request.* / any other member access reads per-bar or
1493
+ # dynamic state.
1494
+ return False
1495
+ if isinstance(node, Subscript):
1496
+ # History read (``close[1]``) or indexed access — per-bar.
1497
+ return False
1498
+ if isinstance(node, Ternary):
1499
+ return (self._expr_is_stable(node.condition)
1500
+ and self._expr_is_stable(node.true_val)
1501
+ and self._expr_is_stable(node.false_val))
1502
+ if isinstance(node, BinOp):
1503
+ return self._expr_is_stable(node.left) and self._expr_is_stable(node.right)
1504
+ if isinstance(node, UnaryOp):
1505
+ return self._expr_is_stable(node.operand)
1506
+ if isinstance(node, FuncCall):
1507
+ func_name, namespace = self._resolve_callee(node.callee)
1508
+ if namespace == "ta":
1509
+ return False
1510
+ if namespace == "math":
1511
+ if func_name not in self._MATH_STABLE_MEMBERS:
1512
+ return False
1513
+ return all(self._expr_is_stable(a) for a in node.args)
1514
+ if namespace == "timeframe":
1515
+ # ``timeframe.in_seconds()`` (and any other function-form
1516
+ # timeframe member) is a stable per-run scalar — it reflects
1517
+ # the script's resolution, not a per-bar value.
1518
+ if func_name not in self._TF_STABLE_MEMBERS:
1519
+ return False
1520
+ return all(self._expr_is_stable(a) for a in node.args)
1521
+ if namespace == "input":
1522
+ return True
1523
+ if namespace is None and func_name in ("int", "float", "bool", "string"):
1524
+ return all(self._expr_is_stable(a) for a in node.args)
1525
+ # Any other call (user functions, str.*, array.*, ...) — series
1526
+ # by default; the conservative answer keeps the guard honest.
1527
+ return False
1528
+ return False
1529
+
1530
+ # AST node kinds whose serialized form is self-delimiting (a literal, a
1531
+ # name, a member read, or a ``name(...)`` call). Non-atomic kinds (BinOp /
1532
+ # UnaryOp / Ternary) MUST be parenthesized when they appear as an operand,
1533
+ # otherwise re-parsing the flattened infix string silently reassociates the
1534
+ # tree: Pine grouping ``(a - b) / (c - d)`` degrades to ``a - b / c - d``
1535
+ # under C++ precedence. See ``_runtime_ctor_arg_for_reset`` (the string is
1536
+ # re-parsed and lowered through the expression visitor).
1537
+ _ATOMIC_ARITH_NODES = (NumberLiteral, Identifier, MemberAccess, FuncCall)
1538
+
1539
+ def _arith_operand_to_str(self, node) -> str | None:
1540
+ """Serialize ``node`` for use as an operand: parenthesize it unless its
1541
+ serialized form is already self-delimiting, so grouping survives a
1542
+ round-trip through the parser."""
1543
+ s = self._arith_expr_to_str(node)
1544
+ if s is None:
1545
+ return None
1546
+ if isinstance(node, self._ATOMIC_ARITH_NODES):
1547
+ return s
1548
+ return f"({s})"
1549
+
1550
+ def _arith_expr_to_str(self, node) -> str | None:
1551
+ """Render a numeric arithmetic-over-identifiers expression to a string
1552
+ that re-parses to the SAME tree (grouping preserved via
1553
+ ``_arith_operand_to_str``). Returns None for any node shape we don't
1554
+ fold (series subscripts, etc.) so the caller leaves the var untracked.
1555
+ """
1556
+ if isinstance(node, NumberLiteral):
1557
+ v = node.value
1558
+ if isinstance(v, float) and v == int(v):
1559
+ return str(int(v))
1560
+ return str(v)
1561
+ if isinstance(node, Identifier):
1562
+ return node.name
1563
+ if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
1564
+ return f"{node.object.name}.{node.member}"
1565
+ if isinstance(node, BinOp):
1566
+ l = self._arith_operand_to_str(node.left)
1567
+ r = self._arith_operand_to_str(node.right)
1568
+ if l is None or r is None:
1569
+ return None
1570
+ return f"{l} {node.op} {r}"
1571
+ if isinstance(node, UnaryOp):
1572
+ o = self._arith_operand_to_str(node.operand)
1573
+ if o is None:
1574
+ return None
1575
+ return f"{node.op}{o}"
1576
+ if isinstance(node, Ternary):
1577
+ c = self._arith_operand_to_str(node.condition)
1578
+ t = self._arith_operand_to_str(node.true_val)
1579
+ f = self._arith_operand_to_str(node.false_val)
1580
+ if c is None or t is None or f is None:
1581
+ return None
1582
+ return f"{c} ? {t} : {f}"
1583
+ if isinstance(node, FuncCall):
1584
+ callee = self._arith_expr_to_str(node.callee)
1585
+ if callee is None:
1586
+ return None
1587
+ parts = []
1588
+ for a in node.args:
1589
+ s = self._arith_expr_to_str(a)
1590
+ if s is None:
1591
+ return None
1592
+ parts.append(s)
1593
+ return f"{callee}({', '.join(parts)})"
1594
+ return None
1595
+
762
1596
  def _collect_known_var(self, node: VarDecl) -> None:
763
1597
  """Extract known constant value from a VarDecl."""
764
1598
  # Don't inline series variables — their values change over time
@@ -811,6 +1645,56 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
811
1645
  if stored:
812
1646
  self._input_backed_vars.add(node.name)
813
1647
  self._input_var_to_call[node.name] = node.value
1648
+ # Class-scope arithmetic / ternaries / casts over known, input-backed,
1649
+ # timeframe.*, or math.* operands
1650
+ # (``wilderLen = rsiLen * 2 - 1``, ``fastPeriod = isM5 ? ... : ...``,
1651
+ # ``filterLen = math.max(1, int(math.round(2 / a)))``).
1652
+ # Without this branch the derived name is untracked, the TA ctor arg
1653
+ # never folds, and the runtime-reset path silently degenerates to a
1654
+ # period of 1. We (a) fold to a literal for the ctor-init list when
1655
+ # possible and (b) record the raw expression so the reset path can
1656
+ # re-expand any input-backed operand to its get_input_*() runtime read
1657
+ # and render timeframe.* / math.* fragments to valid C++.
1658
+ #
1659
+ # The ``_expr_is_stable`` gate is what separates a faithful stable
1660
+ # scalar (inputs + constants + timeframe + math) from a series-derived
1661
+ # value: a length that depends on a ta.* result, a history subscript,
1662
+ # or a bar field stays untracked and is therefore rejected by the TA
1663
+ # ctor guard — preserving the guardrail for genuine dynamic lengths.
1664
+ elif isinstance(node.value, (BinOp, UnaryOp, FuncCall, Ternary)):
1665
+ expr_str = self._arith_expr_to_str(node.value)
1666
+ if expr_str is not None and self._expr_is_stable(node.value):
1667
+ import re as _re
1668
+ tokens = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", expr_str))
1669
+ refs_input = any(t in self._input_backed_vars for t in tokens)
1670
+ refs_derived = any(t in self._derived_input_expr for t in tokens)
1671
+ # The stability classifier already proved this expression is a
1672
+ # bar-invariant scalar (inputs / constants / timeframe.* /
1673
+ # math.* / syminfo.* only). Track it unconditionally so later
1674
+ # stable exprs (and the TA reset path) can reference / expand
1675
+ # it — e.g. ``pi = math.asin(1) * 2`` feeds ``beta`` feeds
1676
+ # ``alpha`` feeds a function-local ``filterLen``.
1677
+ folded = self._resolve_known(expr_str)
1678
+ if self._is_compile_time_value(folded):
1679
+ try:
1680
+ num = float(folded)
1681
+ self._known_vars[node.name] = (
1682
+ int(num) if num == int(num) else num
1683
+ )
1684
+ except ValueError:
1685
+ pass
1686
+ # Record the raw expression so the runtime-reset path can
1687
+ # re-expand operands. Always record for stable derived exprs
1688
+ # (even pure-math / pure-timeframe ones with no input) so the
1689
+ # reset can render them.
1690
+ self._derived_input_expr[node.name] = expr_str
1691
+ self._stable_runtime_vars.add(node.name)
1692
+ # Mark input-backed so use-sites are not inlined and the
1693
+ # override-aware get_input_*() reads are emitted on the reset
1694
+ # path. Pure-math / pure-timeframe exprs (no input) stay out
1695
+ # of this set, which is fine — they have no override to honor.
1696
+ if refs_input or refs_derived:
1697
+ self._input_backed_vars.add(node.name)
814
1698
 
815
1699
  # ------------------------------------------------------------------
816
1700
  # Public entry point
@@ -867,11 +1751,172 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
867
1751
  walk(v)
868
1752
  walk(self.ctx.ast)
869
1753
 
1754
+ def _func_cpp_base_name(self, fname: str) -> str:
1755
+ """Return the actual emitted C++ base name for a UDF or UDT method."""
1756
+ fi = self._func_info_map.get(fname)
1757
+ if fi is not None and getattr(fi, "is_udt_method", False):
1758
+ return self._emit_udt_method_cpp_name(fi)
1759
+ return self._func_safe_name(fname)
1760
+
1761
+ def _inline_history_contexts_for_owner(self, owner: str | None) -> list[str | None]:
1762
+ """Return every method-emission context that owns one source AST site.
1763
+
1764
+ ``None`` denotes top-level/on_bar or a function emitted exactly once.
1765
+ Stateful UDF clones and fresh nested-helper instances use the same names
1766
+ assigned to ``_current_instance_name`` by ``_emit_func_def`` so lookup
1767
+ while visiting a body is deterministic and cannot collapse call sites.
1768
+ """
1769
+ if owner is None:
1770
+ return [None]
1771
+ if owner in self._dead_func_names:
1772
+ return []
1773
+
1774
+ total_cs = self.ctx.func_call_site_counts.get(owner, 0)
1775
+ cloned = (
1776
+ owner in self.ctx.func_ta_ranges
1777
+ or owner in self.ctx.func_series_vars
1778
+ or owner in self.ctx.func_var_members
1779
+ or owner in self.ctx.func_security_clone_only
1780
+ ) and total_cs > 0
1781
+ if cloned:
1782
+ contexts: list[str | None] = [
1783
+ f"{self._func_cpp_base_name(owner)}_cs{idx}"
1784
+ for idx in range(total_cs)
1785
+ ]
1786
+ else:
1787
+ contexts = [None]
1788
+
1789
+ for inst in self._fresh_instances:
1790
+ if inst["fname"] == owner and inst["name"] not in contexts:
1791
+ contexts.append(inst["name"])
1792
+ return contexts
1793
+
1794
+ def _prepare_inline_history_members(self) -> None:
1795
+ """Pre-register every generated temporary-Series class member.
1796
+
1797
+ Member declarations and the declaration-derived rollback aggregate are
1798
+ emitted before function/on_bar bodies. A source-order AST pass therefore
1799
+ reserves stable names up front. The key includes an emitted UDF context
1800
+ because the same body AST is rendered once per stateful call-site clone.
1801
+ """
1802
+ self._inline_history_members = []
1803
+ self._inline_history_member_by_key = {}
1804
+ counters = {"hist_call": 0, "series_arg": 0}
1805
+
1806
+ def walk_nodes(value):
1807
+ """Yield AST nodes in stable field order, including tuple elements.
1808
+
1809
+ NamingHelper._walk_ast predates several AST containers and is
1810
+ intentionally a best-effort utility. Member pre-registration must
1811
+ be exhaustive because a missed node becomes an undeclared C++
1812
+ member, so use the dataclass field graph directly here.
1813
+ """
1814
+ if isinstance(value, ASTNode):
1815
+ yield value
1816
+ for child in vars(value).values():
1817
+ yield from walk_nodes(child)
1818
+ return
1819
+ if isinstance(value, (list, tuple)):
1820
+ for child in value:
1821
+ yield from walk_nodes(child)
1822
+ return
1823
+ if isinstance(value, dict):
1824
+ for child in value.values():
1825
+ yield from walk_nodes(child)
1826
+ return
1827
+ if isinstance(value, TypeField) and value.default is not None:
1828
+ yield from walk_nodes(value.default)
1829
+
1830
+ owner_by_node: dict[int, str] = {}
1831
+ for fi in self.ctx.func_infos:
1832
+ if fi.node is None:
1833
+ continue
1834
+ for child in walk_nodes(fi.node):
1835
+ owner_by_node[id(child)] = fi.name
1836
+
1837
+ def register(kind: str, source_key: tuple, cpp_type: str,
1838
+ owner: str | None) -> None:
1839
+ if cpp_type not in ("double", "int", "bool"):
1840
+ cpp_type = "double"
1841
+ for context in self._inline_history_contexts_for_owner(owner):
1842
+ key = (kind, *source_key, context)
1843
+ if key in self._inline_history_member_by_key:
1844
+ continue
1845
+ counters[kind] += 1
1846
+ member_name = f"_{kind}_{counters[kind]}"
1847
+ self._inline_history_member_by_key[key] = member_name
1848
+ self._inline_history_members.append({
1849
+ "kind": kind,
1850
+ "member_name": member_name,
1851
+ "cpp_type": cpp_type,
1852
+ "context": context,
1853
+ })
1854
+
1855
+ def actual_args_for(call: FuncCall, params: list[str]) -> list:
1856
+ if call.kwargs:
1857
+ return _merge_kwargs(call.args, call.kwargs, params, lambda arg: arg)
1858
+ return list(call.args)
1859
+
1860
+ for node in walk_nodes(self.ctx.ast):
1861
+ owner = owner_by_node.get(id(node))
1862
+ if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
1863
+ register(
1864
+ "hist_call", (id(node),), self._infer_type(node.object), owner
1865
+ )
1866
+
1867
+ if not isinstance(node, FuncCall):
1868
+ continue
1869
+ func_name, _ = self._resolve_callee(node.callee)
1870
+ fi = self._func_info_map.get(func_name)
1871
+ if fi is None or fi.node is None:
1872
+ continue
1873
+ func_sv = self.ctx.func_series_vars.get(fi.name, set())
1874
+ series_param_indices = {
1875
+ idx for idx, name in enumerate(fi.node.params) if name in func_sv
1876
+ }
1877
+ if not series_param_indices:
1878
+ continue
1879
+ args = actual_args_for(node, list(fi.node.params))
1880
+ for idx, arg in enumerate(args):
1881
+ if idx not in series_param_indices:
1882
+ continue
1883
+ if isinstance(arg, Identifier):
1884
+ if arg.name in BAR_FIELDS or arg.name in BAR_SERIES_PUSH:
1885
+ continue
1886
+ if arg.name in self.ctx.series_vars:
1887
+ continue
1888
+ register(
1889
+ "series_arg", (id(node), idx), self._infer_type(arg), owner
1890
+ )
1891
+
1892
+ def _inline_history_member(self, kind: str, node: ASTNode,
1893
+ arg_idx: int | None = None) -> str:
1894
+ source_key = (id(node),) if arg_idx is None else (id(node), arg_idx)
1895
+ key = (kind, *source_key, self._current_instance_name)
1896
+ member = self._inline_history_member_by_key.get(key)
1897
+ if member is None:
1898
+ raise AssertionError(
1899
+ "missing pre-registered inline history member for "
1900
+ f"{kind} at {getattr(node, 'loc', None)} in context "
1901
+ f"{self._current_instance_name!r}"
1902
+ )
1903
+ return member
1904
+
870
1905
  def generate(self) -> str:
871
1906
  """Generate C++ source from the AnalyzerContext."""
1907
+ # Context-sensitive instance pre-pass (needs the naming helpers populated
1908
+ # in __init__). Computes nested stateful-helper dispatch + fresh instances.
1909
+ self._build_func_instances()
1910
+ self._prepare_inline_history_members()
872
1911
  # Pre-scan for strategy series vars
873
1912
  self._prescan_strategy_series()
874
1913
  self._security_ohlc_hist_fields_by_sec: dict[int, set[str]] = {}
1914
+ # request.security TA call-sites read at a history offset (``ta.ema(...)[k>=1]``).
1915
+ # Maps sec_id -> set of TA call-site indices needing an HTF history Series.
1916
+ self._security_ta_hist_idx_by_sec: dict[int, set[int]] = {}
1917
+ # request.security helper-call results read at a history offset
1918
+ # (``myHelper()[k]``). Maps (sec_id, node-id) -> backing Series metadata.
1919
+ self._security_expr_hist_by_node: dict[tuple[int, int], dict] = {}
875
1920
 
876
1921
  lines: list[str] = []
877
1922
 
@@ -897,6 +1942,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
897
1942
  continue
898
1943
  spec = field_specs.get(f.name) or self._type_spec_from_hint_name(f.type_name)
899
1944
  cpp_type = self._type_spec_to_cpp(spec)
1945
+ # Pine ``int`` is 64-bit (it routinely holds UNIX-ms timestamps
1946
+ # and large bar indices); emit UDT int fields as ``int64_t`` so a
1947
+ # field initialised from ``time``/``current_bar_.timestamp`` does
1948
+ # not truncate / narrow-init.
1949
+ if cpp_type == "int":
1950
+ cpp_type = "int64_t"
900
1951
  if f.default:
901
1952
  default = self._visit_expr(f.default)
902
1953
  else:
@@ -930,6 +1981,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
930
1981
  # 2. Open class
931
1982
  lines.append("class GeneratedStrategy : public BacktestEngine {")
932
1983
  lines.append("public:")
1984
+ _script_state_decl_start = len(lines)
933
1985
 
934
1986
  # request.security state
935
1987
  for item in self._security_calls:
@@ -958,14 +2010,26 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
958
2010
  for field in sorted(
959
2011
  self._security_ohlc_hist_fields_by_sec.get(sec_id, ())
960
2012
  ):
2013
+ ctype = self._security_bar_hist_type(field)
961
2014
  lines.append(
962
- f" Series<double> {self._security_ohlc_hist_series_cpp(sec_id, field)}{_mbb};"
2015
+ f" Series<{ctype}> {self._security_ohlc_hist_series_cpp(sec_id, field)}{_mbb};"
963
2016
  )
2017
+ self._security_ta_hist_idx_by_sec[sec_id] = (
2018
+ self._collect_security_ta_hist_indices(expr_node)
2019
+ )
2020
+ for name in self._security_ta_hist_series_names(sec_id):
2021
+ lines.append(f" Series<double> {name}{_mbb};")
2022
+ self._emit_security_expr_hist_members(sec_id, expr_node, lines, _mbb)
964
2023
  continue
965
2024
  if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
966
2025
  hist_fields: set[str] = set()
967
2026
  for el in expr_node.elements:
968
2027
  hist_fields |= self._collect_security_ohlc_hist_fields(el)
2028
+ for name in item.get("mutable_globals", []) or []:
2029
+ info = self._global_mutable_infos.get(name)
2030
+ if info is not None:
2031
+ for stmt in getattr(info, "source_stmts", []) or []:
2032
+ hist_fields |= self._collect_security_ohlc_hist_fields(stmt)
969
2033
  self._security_ohlc_hist_fields_by_sec[sec_id] = hist_fields
970
2034
  for i, el in enumerate(expr_node.elements):
971
2035
  ctype = self._infer_cpp_type_for_security_elem(el)
@@ -973,15 +2037,31 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
973
2037
  lines.append(f" {ctype} _req_sec_{sec_id}_{i}{{}};")
974
2038
  else:
975
2039
  lines.append(f" {ctype} _req_sec_{sec_id}_{i} = na<double>();")
2040
+ elif returns_tuple and tuple_size and tuple_size > 0:
2041
+ self._security_ohlc_hist_fields_by_sec[sec_id] = (
2042
+ self._collect_security_ohlc_hist_fields_for_call(item)
2043
+ )
2044
+ site = self._get_ta_site(expr_node)
2045
+ ta_name = self._ta_name_from_site(site) if site is not None else ""
2046
+ ctype = TA_TUPLE_RESULT_TYPES.get(ta_name, "std::tuple<double, double>")
2047
+ default = self._security_tuple_result_default(ctype, tuple_size)
2048
+ lines.append(f" {ctype} _req_sec_{sec_id} = {default};")
976
2049
  else:
977
- self._security_ohlc_hist_fields_by_sec[sec_id] = self._collect_security_ohlc_hist_fields(
978
- expr_node
2050
+ self._security_ohlc_hist_fields_by_sec[sec_id] = (
2051
+ self._collect_security_ohlc_hist_fields_for_call(item)
979
2052
  )
980
2053
  lines.append(f" double _req_sec_{sec_id} = na<double>();")
981
2054
  for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
2055
+ ctype = self._security_bar_hist_type(field)
982
2056
  lines.append(
983
- f" Series<double> {self._security_ohlc_hist_series_cpp(sec_id, field)}{_mbb};"
2057
+ f" Series<{ctype}> {self._security_ohlc_hist_series_cpp(sec_id, field)}{_mbb};"
984
2058
  )
2059
+ self._security_ta_hist_idx_by_sec[sec_id] = (
2060
+ self._collect_security_ta_hist_indices(expr_node)
2061
+ )
2062
+ for name in self._security_ta_hist_series_names(sec_id):
2063
+ lines.append(f" Series<double> {name}{_mbb};")
2064
+ self._emit_security_expr_hist_members(sec_id, expr_node, lines, _mbb)
985
2065
 
986
2066
  if self._security_calls:
987
2067
  lines.append(' std::unordered_map<std::string, Series<double>> _security_helper_series_;')
@@ -1005,9 +2085,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1005
2085
  )
1006
2086
 
1007
2087
  # 3. TA members
1008
- for site in self.ctx.ta_call_sites:
2088
+ for _ta_idx, site in enumerate(self.ctx.ta_call_sites):
2089
+ if _ta_idx in self._dead_ta_indices:
2090
+ continue
1009
2091
  lines.append(f" {site.class_name} {site.member_name};")
1010
- if getattr(site, "is_static", False):
2092
+ if self._ta_site_uses_precalc(site):
1011
2093
  vtype = self._ta_return_type(site)
1012
2094
  lines.append(f" std::vector<{vtype}> _precalc_{site.member_name};")
1013
2095
  lines.append(" bool _use_precalc = false;")
@@ -1031,8 +2113,20 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1031
2113
  continue
1032
2114
  seen_var_members.add(name)
1033
2115
  safe = self._safe_name(name)
1034
- # Detect array vars from init expression
1035
- if "array.new" in str(init_str) or "array.from" in str(init_str) or name in self._array_vars:
2116
+ # Detect array vars from init expression. Guard the substring
2117
+ # heuristic against a UDT constructor that merely WRAPS array.new /
2118
+ # array.from in its arguments — e.g.
2119
+ # ``var draw d = draw.new(array.new<line>(), array.new<line>())``
2120
+ # must declare as ``draw``, not ``std::vector<double>``. (Drawing
2121
+ # made this latent collision reachable.)
2122
+ _init_str_s = str(init_str)
2123
+ _is_udt_ctor_init = any(
2124
+ _init_str_s.startswith(f"{u}.new") for u in self._udt_defs
2125
+ )
2126
+ if (not _is_udt_ctor_init) and (
2127
+ "array.new" in _init_str_s or "array.from" in _init_str_s
2128
+ or name in self._array_vars
2129
+ ):
1036
2130
  self._array_vars.add(name)
1037
2131
  lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(name))} {safe};")
1038
2132
  continue
@@ -1063,6 +2157,17 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1063
2157
  # decl as ``double`` and the later ``z = SDZone{...}`` would not
1064
2158
  # compile (assigning SDZone to double).
1065
2159
  init_s = str(init_str)
2160
+ # Drawing handle var member (L-N2): a ``var line x`` declares as the
2161
+ # C++ handle struct (Series<Line> when also history-referenced).
2162
+ # Drawing names are NOT in _udt_defs, so the udt branch below would
2163
+ # self-zero them to double; handle them first.
2164
+ _draw_cpp = DRAWING_TYPE_TO_CPP.get(self._udt_var_types.get(name))
2165
+ if _draw_cpp is not None:
2166
+ if name in self.ctx.series_vars:
2167
+ lines.append(f" Series<{_draw_cpp}> {safe}{_mbb};")
2168
+ else:
2169
+ lines.append(f" {_draw_cpp} {safe};")
2170
+ continue
1066
2171
  udt_type = self._udt_var_types.get(name)
1067
2172
  if udt_type not in self._udt_defs:
1068
2173
  udt_type = None
@@ -1092,7 +2197,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1092
2197
  lines.append(f" Series<{cpp_type}> {safe}{_mbb};")
1093
2198
 
1094
2199
  # 7. Fixnan members
1095
- for site in self.ctx.fixnan_sites:
2200
+ for _fi_idx, site in enumerate(self.ctx.fixnan_sites):
2201
+ if _fi_idx in self._dead_fixnan_indices:
2202
+ continue
1096
2203
  cpp_type = PINE_TYPE_TO_CPP.get(site.pine_type, "double")
1097
2204
  lines.append(f" {cpp_type} {site.member_name} = na<{cpp_type}>();")
1098
2205
 
@@ -1106,12 +2213,26 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1106
2213
  else:
1107
2214
  lines.append(f" Series<double> {svar}{_mbb};")
1108
2215
 
2216
+ # 8a. Synthetic temporary history. Unlike the legacy function-local
2217
+ # static buffers, these members are value-copyable rollback state and
2218
+ # have one identity per source site / emitted UDF variant.
2219
+ for info in self._inline_history_members:
2220
+ lines.append(
2221
+ f" Series<{info['cpp_type']}> {info['member_name']}{_mbb};"
2222
+ )
2223
+
1109
2224
  # 8b. Global-scope non-var declarations as class members
1110
2225
  # (so user-defined functions can reference them)
1111
2226
  seen_global = set()
1112
2227
  for name, ptype in self.ctx.global_var_decls:
1113
2228
  if name in seen_global or name in self.ctx.series_vars or name in self._var_names:
1114
2229
  continue
2230
+ # De-hoisted UDT array-element alias (Pine reference semantics): the
2231
+ # in-loop VarDecl is emitted as a fresh ``UDT& z = arr[i];`` local
2232
+ # reference each iteration, so there is no persistent class member.
2233
+ if name in self._udt_array_get_ref_locals:
2234
+ seen_global.add(name)
2235
+ continue
1115
2236
  seen_global.add(name)
1116
2237
  safe = self._safe_name(name)
1117
2238
 
@@ -1131,7 +2252,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1131
2252
  # data/validation/udt-method-probe-19-array-of-udt-method,
1132
2253
  # data/validation/udt-method-probe-20-udt-return-from-func.
1133
2254
  udt_t = self._udt_var_types[name]
1134
- lines.append(f" {udt_t} {safe} = {udt_t}{{}};")
2255
+ # Drawing handle global (L-N6 / U): map line/box/label/linefill
2256
+ # to the C++ handle struct (the default is na, id=-1).
2257
+ _draw_cpp = DRAWING_TYPE_TO_CPP.get(udt_t)
2258
+ if _draw_cpp is not None:
2259
+ lines.append(f" {_draw_cpp} {safe} = {_draw_cpp}{{}};")
2260
+ else:
2261
+ lines.append(f" {udt_t} {safe} = {udt_t}{{}};")
1135
2262
  else:
1136
2263
  expr = self.ctx.global_expr_map.get(name) if hasattr(self.ctx, "global_expr_map") else None
1137
2264
  cpp_type = self._infer_type(expr) if expr is not None else PINE_TYPE_TO_CPP.get(ptype, "double")
@@ -1148,37 +2275,65 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1148
2275
  if cloned_safe in emitted_clones:
1149
2276
  continue # already declared by another function's clone
1150
2277
  emitted_clones.add(cloned_safe)
1151
- # Determine the type by finding the original declaration
1152
- orig_name = orig_safe # _safe_name was already applied
1153
- # Check if it's a var member (Series) or plain series
1154
- found = False
1155
- for vname, ptype, init_str in self.ctx.var_members:
1156
- if self._safe_name(vname) == orig_safe:
1157
- cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
1158
- if vname in self.ctx.series_vars:
1159
- lines.append(f" Series<{cpp_type}> {cloned_safe}{_mbb};")
1160
- elif vname in self._matrix_specs:
1161
- lines.append(f" {self._type_spec_to_cpp(self._matrix_specs[vname])} {cloned_safe};")
1162
- elif vname in self._array_vars:
1163
- lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(vname))} {cloned_safe};")
1164
- elif vname in self._map_vars:
1165
- lines.append(f" {self._type_spec_to_cpp(self._map_spec_for_name(vname))} {cloned_safe};")
1166
- else:
1167
- lines.append(f" {cpp_type} {cloned_safe};")
1168
- found = True
1169
- break
1170
- if not found:
1171
- # Non-var series var
1172
- if orig_safe in [self._safe_name(n) for n in self.ctx.series_vars]:
1173
- cpp_type = self._series_type_for(orig_safe)
1174
- lines.append(f" Series<{cpp_type}> {cloned_safe}{_mbb};")
1175
- else:
1176
- lines.append(f" double {cloned_safe} = 0.0;")
2278
+ self._emit_cloned_var_decl(orig_safe, cloned_safe, _mbb, lines)
2279
+
2280
+ # 8c2. Fresh var members for context-sensitive helper instances (nested
2281
+ # helpers reached through >1 distinct call path). Each fresh instance
2282
+ # gets its OWN scalar/series state so two paths never collide.
2283
+ for orig_safe, fresh_safe in self._fresh_var_members:
2284
+ if fresh_safe in emitted_clones:
2285
+ continue
2286
+ emitted_clones.add(fresh_safe)
2287
+ self._emit_cloned_var_decl(orig_safe, fresh_safe, _mbb, lines)
2288
+
2289
+ # 8c3. Fresh fixnan members for context-sensitive helper instances.
2290
+ # Each fresh instance gets its OWN previous-value member so two
2291
+ # call paths never share fixnan state (mirrors 8c2 for vars).
2292
+ for orig_site, fresh_safe in self._fresh_fixnan_members:
2293
+ if fresh_safe in emitted_clones:
2294
+ continue
2295
+ emitted_clones.add(fresh_safe)
2296
+ cpp_type = PINE_TYPE_TO_CPP.get(orig_site.pine_type, "double")
2297
+ lines.append(f" {cpp_type} {fresh_safe} = na<{cpp_type}>();")
2298
+
2299
+ # 8d. Drawing-objects-as-data arenas (gated on _uses_drawing so
2300
+ # non-drawing strategies emit byte-identical C++). Each arena is a
2301
+ # per-strategy member -> reset-per-run is automatic. Caps come from
2302
+ # the strategy() header max_*_count (default 50; linefill default 50).
2303
+ if self._uses_drawing:
2304
+ caps = self._drawing_caps or {}
2305
+ lines.append(f" DrawingArena<LineRec> _pf_lines_{{{caps.get('line', 50)}}};")
2306
+ lines.append(f" DrawingArena<BoxRec> _pf_boxes_{{{caps.get('box', 50)}}};")
2307
+ lines.append(f" DrawingArena<LabelRec> _pf_labels_{{{caps.get('label', 50)}}};")
2308
+ lines.append(f" DrawingArena<LinefillRec> _pf_linefills_{{{caps.get('linefill', 50)}}};")
1177
2309
 
1178
2310
  # 9. _var_initialized flag
1179
2311
  if self.ctx.var_members:
1180
2312
  lines.append(" bool _var_initialized = false;")
1181
2313
 
2314
+ # 9a. Per-function-variant ``var`` init flags. A function-scoped
2315
+ # ``var`` (Pine "init once" semantics) is a function-local static:
2316
+ # its initializer runs on the FIRST call to that function variant
2317
+ # (with the first bar's values the function actually sees) and the
2318
+ # result persists for the strategy's lifetime. Each clone (cs0,
2319
+ # cs1, ...) is an independent instance with its own flag.
2320
+ # ``func_var_members`` is keyed by the plain Pine function name
2321
+ # (``fi.name``), so this matches both plain UDFs and UDT methods.
2322
+ for fi in self.ctx.func_infos:
2323
+ if fi.name not in self.ctx.func_var_members:
2324
+ continue
2325
+ total_cs = self.ctx.func_call_site_counts.get(fi.name, 0)
2326
+ if total_cs > 0:
2327
+ for cs_idx in range(total_cs):
2328
+ lines.append(f" bool _fvinit_{self._func_safe_name(fi.name)}_cs{cs_idx} = false;")
2329
+ else:
2330
+ lines.append(f" bool _fvinit_{self._func_safe_name(fi.name)} = false;")
2331
+
2332
+ # 9a2. ``var`` init flags for fresh context-sensitive helper instances.
2333
+ for inst in self._fresh_instances:
2334
+ if inst["fname"] in self.ctx.func_var_members and inst["var_remap"]:
2335
+ lines.append(f" bool _fvinit_{inst['name']} = false;")
2336
+
1182
2337
  # 9b. _ta_initialized_ flag for runtime TA re-sizing (first on_bar only).
1183
2338
  if self.ctx.ta_call_sites:
1184
2339
  lines.append(" bool _ta_initialized_ = false;")
@@ -1188,6 +2343,16 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1188
2343
 
1189
2344
  lines.append("")
1190
2345
 
2346
+ # 9d. Historical execution rollback checkpoint. Derive the member
2347
+ # inventory from the declarations above so every future generated
2348
+ # state category is captured automatically (or generation fails loudly
2349
+ # if it introduces an unfamiliar declaration form).
2350
+ _script_state_members = self._collect_script_state_members(
2351
+ lines[_script_state_decl_start:-1]
2352
+ )
2353
+ self._emit_script_state_hooks(lines, _script_state_members)
2354
+ lines.append("")
2355
+
1191
2356
  # 9. Constructor with TA initializer list
1192
2357
  self._emit_constructor(lines)
1193
2358
  lines.append("")
@@ -1195,10 +2360,24 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1195
2360
  # 10. User-defined functions (with per-call-site variants for functions
1196
2361
  # containing TA calls OR series variables that need isolation)
1197
2362
  for fi in self.ctx.func_infos:
2363
+ # Dead-code user functions (defined but never called, with TA
2364
+ # state whose ctor args can't be sized) are skipped entirely —
2365
+ # their bodies reference TA members we no longer emit, and the
2366
+ # functions never run anyway.
2367
+ if fi.name in self._dead_func_names:
2368
+ continue
1198
2369
  total_cs = self.ctx.func_call_site_counts.get(fi.name, 0)
1199
2370
  has_ta = fi.name in self.ctx.func_ta_ranges
1200
2371
  has_series = fi.name in self.ctx.func_series_vars or fi.name in self.ctx.func_var_members
1201
- if (has_ta or has_series) and total_cs > 0:
2372
+ # A function whose ONLY reason to need per-call-site cloning is a
2373
+ # security-tf-monomorphized request.security (no TA/series state
2374
+ # of its own — see Analyzer._check_mixed_callsite_security_tf)
2375
+ # still needs N separate emitted bodies so self._active_call_site_idx
2376
+ # is set while each is emitted (read by the request.security
2377
+ # use-site lowering in visit_call.py to pick the right clone's
2378
+ # sec_id).
2379
+ needs_security_clone = fi.name in self.ctx.func_security_clone_only
2380
+ if (has_ta or has_series or needs_security_clone) and total_cs > 0:
1202
2381
  # Emit one variant per call site
1203
2382
  for cs_idx in range(total_cs):
1204
2383
  self._emit_func_def(fi, lines, call_site_idx=cs_idx)
@@ -1207,7 +2386,20 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1207
2386
  self._emit_func_def(fi, lines)
1208
2387
  lines.append("")
1209
2388
 
2389
+ # 10a. Fresh context-sensitive instances of nested stateful helpers
2390
+ # (reached through >1 distinct call path). Each is bound to its own
2391
+ # path-specific TA + var members; see _build_func_instances.
2392
+ if self._fresh_instances:
2393
+ fi_by_name = {fi.name: fi for fi in self.ctx.func_infos}
2394
+ for inst in self._fresh_instances:
2395
+ fi = fi_by_name.get(inst["fname"])
2396
+ if fi is None:
2397
+ continue
2398
+ self._emit_func_def(fi, lines, instance=inst)
2399
+ lines.append("")
2400
+
1210
2401
  # 11. on_bar()
2402
+ self._current_instance_name = None
1211
2403
  self._emit_on_bar(lines)
1212
2404
  lines.append("")
1213
2405
 
@@ -1319,13 +2511,22 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1319
2511
  def _is_skip_expr(self, node) -> bool:
1320
2512
  """Check if an expression should be skipped (visual/unsupported)."""
1321
2513
  if isinstance(node, FuncCall):
2514
+ # chart.point.* resolves to namespace "chart" (a SKIP_NAMESPACE) but
2515
+ # is REAL data (a ChartPoint aggregate literal). Never skip it.
2516
+ if self._is_chart_point_callee(node.callee):
2517
+ return False
1322
2518
  func_name, namespace = self._resolve_callee(node.callee)
1323
- if func_name in SKIP_FUNC_NAMES:
2519
+ if namespace is None and func_name in SKIP_FUNC_NAMES:
1324
2520
  return True
1325
2521
  if namespace in SKIP_NAMESPACES:
1326
2522
  return True
1327
2523
  if namespace in SKIP_VAR_TYPES:
1328
2524
  return True
2525
+ # Method call on a table/polyline-typed receiver var/param
2526
+ # (``panel.cell(...)``). These types have no C++ representation, so
2527
+ # the call is a visual no-op — drop it (mirrors the namespace form).
2528
+ if namespace in self._visual_drop_vars:
2529
+ return True
1329
2530
  # strategy.risk.* — handled in _visit_stmt, not skipped
1330
2531
  if isinstance(node, MemberAccess):
1331
2532
  if isinstance(node.object, Identifier) and node.object.name in SKIP_NAMESPACES:
@@ -1363,59 +2564,335 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1363
2564
  # _infer_type / _infer_tuple_types live on TypeInferer — see codegen/types.py.
1364
2565
  # _is_compile_time_value lives on TaSiteHelper — see codegen/ta.py.
1365
2566
 
2567
+ # Pine ``timeframe.<member>`` -> C++ runtime expression. Mirrors the
2568
+ # mapping in ``visit_expr._visit_member_access`` so a stable timeframe
2569
+ # fragment embedded in a TA ctor reset renders to the same C++ the
2570
+ # expression visitor would emit for a direct ``timeframe.*`` read.
2571
+ _TIMEFRAME_MEMBER_CPP: dict[str, str] = {
2572
+ "period": "script_tf_",
2573
+ "main_period": "main_period()",
2574
+ "multiplier": "tf_multiplier(script_tf_)",
2575
+ "isintraday": "tf_is_intraday(script_tf_)",
2576
+ "isminutes": "(tf_is_intraday(script_tf_) && !tf_is_seconds(script_tf_))",
2577
+ "isdaily": "tf_is_daily(script_tf_)",
2578
+ "isweekly": "tf_is_weekly(script_tf_)",
2579
+ "ismonthly": "tf_is_monthly(script_tf_)",
2580
+ "isdwm": "(tf_is_daily(script_tf_) || tf_is_weekly(script_tf_) || tf_is_monthly(script_tf_))",
2581
+ "isseconds": "tf_is_seconds(script_tf_)",
2582
+ "in_seconds": "tf_to_seconds(script_tf_)",
2583
+ "isticks": "false",
2584
+ }
2585
+
2586
+ # Pine ``math.<member>`` -> C++ form. Function members map to ``std::*``;
2587
+ # constants map to their engine-side macro / literal.
2588
+ _MATH_MEMBER_CPP: dict[str, str] = {
2589
+ "pi": "M_PI", "e": "M_E", "phi": "1.618033988749895",
2590
+ "rphi": "0.6180339887498949",
2591
+ "abs": "std::abs", "max": "std::max", "min": "std::min",
2592
+ "round": "std::round", "floor": "std::floor", "ceil": "std::ceil",
2593
+ "sqrt": "std::sqrt", "log": "std::log", "log10": "std::log10",
2594
+ "exp": "std::exp", "pow": "std::pow",
2595
+ "sin": "std::sin", "cos": "std::cos", "tan": "std::tan",
2596
+ "asin": "std::asin", "acos": "std::acos", "atan": "std::atan",
2597
+ "sign": "(double)([] (double _v) { return (_v>0) - (_v<0); })",
2598
+ }
2599
+
2600
+ # Pine logical operators (word form) -> C++ operator, used when rendering
2601
+ # a stable runtime expression. Matched with word boundaries.
2602
+ _PINE_LOGICAL_OPS: dict[str, str] = {"and": "&&", "or": "||", "not": "!"}
2603
+
2604
+ def _render_inline_input_calls(self, expr_str: str) -> tuple[str, bool]:
2605
+ """Render inline ``input(...)`` / ``input.<type>(...)`` calls in a TA
2606
+ ctor-arg expression string to override-aware ``get_input_*()`` reads.
2607
+
2608
+ A bare input expression passed straight as a length argument
2609
+ (``adx(input(15), input(15))``) reaches the reset path as the raw call
2610
+ spelling ``input(15)`` because the analyzer's param-substitution has no
2611
+ intermediate variable to record in ``_input_backed_vars``. This helper
2612
+ finds each such call (balanced parens, ``input`` optionally followed by
2613
+ ``.<type>``), re-parses it into a FuncCall, and renders it via the same
2614
+ ``_render_input_value`` used for ordinary input var reads.
2615
+
2616
+ Returns ``(rewritten_str, found_any)``. When no inline input call is
2617
+ present, the string is returned unchanged with ``found_any=False``.
2618
+ """
2619
+ import re
2620
+ # Locate ``input`` (as a word, not a substring of get_input_int etc.)
2621
+ # optionally followed by ``.<member>``, then a ``(`` opening a balanced
2622
+ # argument list.
2623
+ out = expr_str
2624
+ found = False
2625
+ idx = 0
2626
+ while idx < len(out):
2627
+ m = re.search(r"\binput\b", out[idx:])
2628
+ if m is None:
2629
+ break
2630
+ start = idx + m.start()
2631
+ # Reject a match that is part of a longer identifier
2632
+ # (e.g. ``get_input_int``) — the \b guard above already handles
2633
+ # alphanumerics, but be defensive.
2634
+ if start > 0 and (out[start - 1].isalnum() or out[start - 1] == "_"):
2635
+ idx = start + len("input")
2636
+ continue
2637
+ j = start + len("input")
2638
+ # Optional ``.<member>`` for the typed form ``input.int(...)``.
2639
+ member = None
2640
+ if j < len(out) and out[j] == ".":
2641
+ k = j + 1
2642
+ nm_start = k
2643
+ while k < len(out) and (out[k].isalnum() or out[k] == "_"):
2644
+ k += 1
2645
+ if k > nm_start:
2646
+ member = out[nm_start:k]
2647
+ j = k
2648
+ # Must be followed by ``(`` to be a call.
2649
+ if j >= len(out) or out[j] != "(":
2650
+ idx = j
2651
+ continue
2652
+ # Walk the balanced parens to extract the call substring.
2653
+ depth = 0
2654
+ k = j
2655
+ while k < len(out):
2656
+ ch = out[k]
2657
+ if ch == "(":
2658
+ depth += 1
2659
+ elif ch == ")":
2660
+ depth -= 1
2661
+ if depth == 0:
2662
+ k += 1
2663
+ break
2664
+ k += 1
2665
+ if depth != 0:
2666
+ # Unbalanced — bail on this match.
2667
+ idx = j + 1
2668
+ continue
2669
+ call_src = out[start:k]
2670
+ try:
2671
+ from ..lexer import Lexer
2672
+ from ..parser import Parser
2673
+ tokens = Lexer(call_src).tokenize()
2674
+ node = Parser(tokens, source=call_src)._parse_expression()
2675
+ if not self._is_input_call(node):
2676
+ idx = k
2677
+ continue
2678
+ func_name_i, namespace_i = self._resolve_callee(node.callee)
2679
+ # Inline inputs have no enclosing var; reuse the default
2680
+ # value as a synthetic title key so distinct defaults get
2681
+ # distinct input controls (and identical defaults collapse,
2682
+ # which is correct since they resolve to the same value).
2683
+ default_node = self._get_input_default(node)
2684
+ synth_title = self._visit_expr(default_node) if default_node is not None else ""
2685
+ title = self._get_input_title(node, var_name=None)
2686
+ if not title:
2687
+ title = synth_title
2688
+ rendered = self._render_input_value(node, func_name_i, namespace_i, title)
2689
+ except Exception:
2690
+ idx = k
2691
+ continue
2692
+ out = out[:start] + rendered + out[k:]
2693
+ found = True
2694
+ idx = start + len(rendered)
2695
+ return out, found
2696
+
1366
2697
  def _runtime_ctor_arg_for_reset(self, arg_str: str) -> str | None:
1367
- """Convert a TA ctor-arg string into its runtime C++ expression when
1368
- the source expression references an input-backed variable. Returns the
1369
- runtime expression (e.g. ``get_input_int("MACD Fast", 12)``) when the
1370
- ctor arg depends on an input value; returns None for pure literals or
1371
- expressions that do not contain any input-backed identifier, so the
1372
- caller can decide to skip emitting a reset for that site.
2698
+ """Convert a TA ctor-arg string into its runtime C++ expression.
2699
+
2700
+ Returns the runtime expression (e.g.
2701
+ ``get_input_int("MACD Fast", 12)`` or a ternary / math expression
2702
+ over such reads and ``timeframe.*`` members) when the ctor arg
2703
+ depends on a stable runtime scalar — an input-backed variable, a
2704
+ ``timeframe.*`` member, or arithmetic / ternaries / casts over
2705
+ those. Returns None for pure literals or expressions that contain
2706
+ any unrecognised (potentially series) identifier, so the caller
2707
+ (the TA ctor guard) rejects them loudly instead of silently
2708
+ emitting period 1.
1373
2709
  """
1374
2710
  import re
1375
2711
  ident_re = re.compile(r"[A-Za-z_][A-Za-z_0-9]*")
1376
- tokens = ident_re.findall(arg_str)
1377
- input_tokens = [t for t in tokens if t in self._input_backed_vars]
1378
- if not input_tokens:
2712
+
2713
+ # Expand class-scope derived vars (``wilderLen`` -> ``(rsiLen * 2 - 1)``,
2714
+ # ``fastPeriod`` -> ``(isM5 ? ... : ...)``) to their raw RHS so input
2715
+ # leaves become get_input_*() reads below. Recursive (bounded) to
2716
+ # handle chains of derived vars; guards against cycles.
2717
+ def _expand_derived(s: str, seen: frozenset = frozenset(), depth: int = 0) -> str:
2718
+ if depth > 32:
2719
+ return s
2720
+ def _rep(p: re.Match) -> str:
2721
+ nm = p.group(0)
2722
+ if nm in self._derived_input_expr and nm not in seen:
2723
+ inner = self._derived_input_expr[nm]
2724
+ return "(" + _expand_derived(inner, seen | {nm}, depth + 1) + ")"
2725
+ return nm
2726
+ return ident_re.sub(_rep, s)
2727
+
2728
+ expanded = _expand_derived(arg_str)
2729
+
2730
+ tokens = set(ident_re.findall(expanded))
2731
+
2732
+ # Gate: every identifier token must be renderable. If any token is an
2733
+ # unrecognised bare identifier (not an input, not a known const, not
2734
+ # a structural keyword / namespace prefix, not a stable tracked var
2735
+ # that we already expanded), we conservatively refuse — that identifier
2736
+ # would otherwise leak through as an undeclared C++ symbol, or worse,
2737
+ # a series var that should have been rejected by the ctor guard.
2738
+ structural = (set(self._PINE_LOGICAL_OPS)
2739
+ | {"timeframe", "math", "syminfo",
2740
+ "int", "float", "bool", "string",
2741
+ "true", "false", "na"})
2742
+ member_tokens = (set(self._TIMEFRAME_MEMBER_CPP)
2743
+ | set(self._MATH_MEMBER_CPP))
2744
+ renderable = (self._input_backed_vars
2745
+ | self._stable_runtime_vars
2746
+ | structural
2747
+ | member_tokens)
2748
+ leftover = tokens - renderable
2749
+ # Known compile-time consts that survived expansion (e.g. ``pi`` was
2750
+ # NOT tracked as a Python value but its name token is a stable var
2751
+ # already; pure numeric names are in _known_vars and covered above).
2752
+ leftover = {t for t in leftover if t not in self._known_vars}
2753
+ # Inline ``input(...)`` / ``input.<t>(...)`` calls (a bare input
2754
+ # expression passed straight as a length arg, e.g.
2755
+ # ``adx(input(15), input(15))``) are re-parsed and rendered below,
2756
+ # after the gate. ``input`` is the only token they contribute, so
2757
+ # allow it through the gate here.
2758
+ leftover.discard("input")
2759
+ if leftover:
1379
2760
  return None
1380
2761
 
1381
- # Pine math.* → C++ std::* (must run before identifier substitution so
1382
- # we don't treat `math.round` etc. as a bare identifier). We wrap the
1383
- # whole expression in (int) below because TA ctors want integer lengths.
1384
- expr = arg_str
1385
- math_map = {
1386
- "math.round": "std::round",
1387
- "math.sqrt": "std::sqrt",
1388
- "math.ceil": "std::ceil",
1389
- "math.floor": "std::floor",
1390
- "math.abs": "std::abs",
1391
- "math.max": "std::max",
1392
- "math.min": "std::min",
1393
- "math.log": "std::log",
1394
- "math.exp": "std::exp",
1395
- "math.pow": "std::pow",
1396
- }
1397
- for pine_fn, cpp_fn in math_map.items():
1398
- expr = expr.replace(pine_fn, cpp_fn)
1399
-
1400
- def _sub(match: re.Match) -> str:
1401
- name = match.group(0)
1402
- if name not in self._input_backed_vars:
1403
- return name
1404
- call_node = self._input_var_to_call.get(name)
1405
- if call_node is None:
1406
- return name
1407
- func_name_i, namespace_i = self._resolve_callee(call_node.callee)
1408
- title = self._get_input_title(call_node, var_name=name)
1409
- return self._render_input_value(call_node, func_name_i, namespace_i, title)
2762
+ # Must depend on at least one runtime component (input-backed var, a
2763
+ # timeframe reference, or an inline input() call); otherwise it's a
2764
+ # pure compile-time expr and no reset is needed (the ctor-init literal
2765
+ # is correct).
2766
+ has_input = any(t in self._input_backed_vars for t in tokens)
2767
+ has_timeframe = "timeframe" in tokens
2768
+ has_inline_input = "input" in tokens
2769
+ if not (has_input or has_timeframe or has_inline_input):
2770
+ return None
2771
+
2772
+ # Preferred path: re-parse the (gate-approved, grouping-faithful)
2773
+ # expression and lower it through the SAME expression visitor the
2774
+ # statement path uses. Operator grouping and Pine numeric typing
2775
+ # (``/`` always yields float; ``(double)`` coercion) are then correct
2776
+ # by construction — reused from ``_visit_binop``, not re-derived here.
2777
+ # Falls back to the legacy token-substitution renderer below only if
2778
+ # re-parse / lowering unexpectedly fails, so a working site is never
2779
+ # worse off than before this change.
2780
+ lowered = self._lower_reset_expr_via_visitor(expanded)
2781
+ if lowered is not None:
2782
+ return lowered
2783
+
2784
+ # Render inline input() calls to override-aware get_input_*() reads
2785
+ # now that the gate has accepted the expression. Done after the gate
2786
+ # so the rendered getter tokens (get_input_int, ...) do not have to
2787
+ # be added to ``renderable``.
2788
+ if has_inline_input:
2789
+ expanded, _ = self._render_inline_input_calls(expanded)
2790
+
2791
+ expr = expanded
2792
+
2793
+ # 1) timeframe.<member> -> C++ (before ident substitution so the
2794
+ # member names don't get caught by the identifier pass). Use a
2795
+ # targeted regex so e.g. ``isminutes`` is not confused with
2796
+ # ``ismonthly``.
2797
+ def _tf_rep(p: re.Match) -> str:
2798
+ mem = p.group(1)
2799
+ return self._TIMEFRAME_MEMBER_CPP.get(mem, p.group(0))
2800
+ # ``timeframe.in_seconds()`` is a function-form member in Pine (the
2801
+ # only one in the table); its C++ form ``tf_to_seconds(script_tf_)``
2802
+ # is already a complete call, so consume the Pine ``()`` to avoid a
2803
+ # double-call ``tf_to_seconds(script_tf_)()``. Property-form members
2804
+ # (``timeframe.isdaily``) never carry ``()`` so the optional group is
2805
+ # a no-op for them.
2806
+ expr = re.sub(r"\btimeframe\.(\w+)(?:\(\))?", _tf_rep, expr)
2807
+
2808
+ # 2) math.<member> -> C++ (constants + std::* functions).
2809
+ def _math_rep(p: re.Match) -> str:
2810
+ mem = p.group(1)
2811
+ return self._MATH_MEMBER_CPP.get(mem, p.group(0))
2812
+ expr = re.sub(r"\bmath\.(\w+)", _math_rep, expr)
2813
+
2814
+ # 3) Pine word-logical operators -> C++ operators (after timeframe/math
2815
+ # substitution so we don't rewrite inside their C++ expansions).
2816
+ for pine_op, cpp_op in self._PINE_LOGICAL_OPS.items():
2817
+ expr = re.sub(rf"\b{pine_op}\b", cpp_op, expr)
2818
+
2819
+ # 4) Substitute input-backed vars with override-aware get_input_*()
2820
+ # reads, and inline known compile-time consts (non-input) as literals.
2821
+ def _sub(p: re.Match) -> str:
2822
+ name = p.group(0)
2823
+ if name in self._input_backed_vars:
2824
+ call_node = self._input_var_to_call.get(name)
2825
+ if call_node is None:
2826
+ return name
2827
+ func_name_i, namespace_i = self._resolve_callee(call_node.callee)
2828
+ title = self._get_input_title(call_node, var_name=name)
2829
+ return self._render_input_value(call_node, func_name_i, namespace_i, title)
2830
+ if name in self._known_vars and name not in self._input_backed_vars:
2831
+ val = self._known_vars[name]
2832
+ if isinstance(val, bool):
2833
+ return "true" if val else "false"
2834
+ if isinstance(val, (int, float)):
2835
+ return str(val)
2836
+ return f'std::string("{self._cpp_string_escape(val)}")'
2837
+ return name
1410
2838
 
1411
2839
  rewritten = ident_re.sub(_sub, expr)
1412
- # Pine auto-converts floats to ints for TA lengths; C++ does not, so
1413
- # wrap the whole expression in an explicit int cast when any math.*
1414
- # function appears (they return doubles).
1415
- if any(m in arg_str for m in math_map):
2840
+
2841
+ # 5) Pine auto-converts floats to ints for TA lengths; C++ does not.
2842
+ # If any math.* function appears (returns double) OR a timeframe.*
2843
+ # boolean is part of a ternary whose branches are doubles, wrap the
2844
+ # whole expression in an explicit int cast so the TA ctor gets an
2845
+ # integer length.
2846
+ had_math = "std::" in rewritten or bool(re.search(r"\btimeframe\b", expanded))
2847
+ if had_math:
1416
2848
  return f"(int)({rewritten})"
1417
2849
  return rewritten
1418
2850
 
2851
+ def _lower_reset_expr_via_visitor(self, expanded: str) -> str | None:
2852
+ """Re-parse a gate-approved, fully-expanded TA-length expression and
2853
+ lower it through the SAME expression visitor the statement path uses,
2854
+ so operator grouping and Pine numeric typing are preserved identically
2855
+ (Pine ``/`` always yields float; ``math.*`` returns double; branches are
2856
+ parenthesized). Reuses ``_visit_binop`` etc.; nothing is re-typed here.
2857
+
2858
+ Input-backed variables render as override-aware ``get_input_*()`` reads
2859
+ (not member refs) via ``_reset_input_getter_mode`` — the reset can run
2860
+ in ``evaluate_security`` before the input members are initialised, so it
2861
+ must not depend on their init order. Inline ``input(...)`` calls and
2862
+ ``math.*`` / ``timeframe.*`` members lower through the visitor's own
2863
+ handlers (same C++ the statement path would emit).
2864
+
2865
+ Returns None if re-parse / lowering fails, so the caller falls back to
2866
+ the legacy token-substitution renderer (never worse than before)."""
2867
+ try:
2868
+ from ..lexer import Lexer
2869
+ from ..parser import Parser
2870
+ tokens = Lexer(expanded).tokenize()
2871
+ node = Parser(tokens, source=expanded)._parse_expression()
2872
+ except Exception:
2873
+ return None
2874
+ prev = self._reset_input_getter_mode
2875
+ self._reset_input_getter_mode = True
2876
+ try:
2877
+ rendered = self._visit_expr(node)
2878
+ except Exception:
2879
+ return None
2880
+ finally:
2881
+ self._reset_input_getter_mode = prev
2882
+ if not rendered or "/* " in rendered:
2883
+ # Unknown/unhandled node leaked a placeholder — defer to legacy.
2884
+ return None
2885
+ # A TA length must be int. Truncate when the lowered form is
2886
+ # float-typed (a ``std::*`` call, a ``(double)`` division coercion, or a
2887
+ # ``timeframe.*`` helper — all timeframe helpers reference script_tf_).
2888
+ # A bare int / identifier length carries none of these and is left
2889
+ # unwrapped, so simple sites stay byte-identical to the legacy output.
2890
+ if ("std::" in rendered or "(double)" in rendered
2891
+ or "script_tf_" in rendered):
2892
+ return f"(int)({rendered})"
2893
+ return rendered
2894
+
2895
+
1419
2896
  def _collect_ta_runtime_resets(self) -> list[str]:
1420
2897
  """Collect reassignment statements for every TA object whose ctor args
1421
2898
  depend on an input-backed variable. Returned strings are raw C++
@@ -1427,7 +2904,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1427
2904
  resets: list[str] = []
1428
2905
 
1429
2906
  # Main-context TA objects
1430
- for site in self.ctx.ta_call_sites:
2907
+ for _ta_idx, site in enumerate(self.ctx.ta_call_sites):
2908
+ if _ta_idx in self._dead_ta_indices:
2909
+ continue
1431
2910
  if not site.ctor_args:
1432
2911
  continue
1433
2912
  runtime_args: list[str] = []
@@ -1445,15 +2924,38 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1445
2924
  f"{site.member_name} = {site.class_name}({', '.join(runtime_args)});"
1446
2925
  )
1447
2926
 
1448
- # Security-context TA copies (same ctor args as their main-context site)
2927
+ # Security-context TA copies. Normally these share the ctor args of
2928
+ # their main-context site, but a request.security nested in a helper
2929
+ # called at several sites is cloned per call site (distinct sec_id +
2930
+ # callsite_idx) while the shared TA site's ``ctor_args`` were resolved
2931
+ # ONCE (against the first call site). Every clone would then size its
2932
+ # indicator from call site 0's argument (e.g. four EMAs all pinned to
2933
+ # crossFastLen instead of the per-site fast/slow lengths). Resolve each
2934
+ # sec's ctor args against ITS call site by reusing the per-call-site
2935
+ # function-clone TA remap (identity for cs0 / non-clones, so all other
2936
+ # output stays byte-identical).
2937
+ sec_call_by_id = {it["sec_id"]: it for it in self._security_calls}
2938
+ ta_site_by_member = {s.member_name: s for s in self.ctx.ta_call_sites}
1449
2939
  for info in self._security_eval_info:
2940
+ sec_item = sec_call_by_id.get(info["sec_id"])
2941
+ sec_containing = (sec_item or {}).get("containing_func") or ""
2942
+ sec_cs_idx = (sec_item or {}).get("callsite_idx")
1450
2943
  for idx, variants in (info.get("ta_variants") or {}).items():
1451
2944
  site = self.ctx.ta_call_sites[idx]
1452
- if not site.ctor_args:
2945
+ ctor_site = site
2946
+ if sec_containing and sec_cs_idx is not None:
2947
+ remap = self._func_cs_ta_remap.get((sec_containing, sec_cs_idx))
2948
+ if remap:
2949
+ cloned_name = remap.get(site.member_name)
2950
+ if cloned_name and cloned_name != site.member_name:
2951
+ cand = ta_site_by_member.get(cloned_name)
2952
+ if cand is not None:
2953
+ ctor_site = cand
2954
+ if not ctor_site.ctor_args:
1453
2955
  continue
1454
2956
  runtime_args = []
1455
2957
  any_runtime = False
1456
- for a in site.ctor_args:
2958
+ for a in ctor_site.ctor_args:
1457
2959
  rt = self._runtime_ctor_arg_for_reset(a)
1458
2960
  if rt is not None:
1459
2961
  runtime_args.append(rt)