@pineforge/codegen-pyodide 0.8.0 → 0.8.1

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,
@@ -112,12 +113,17 @@ from .visit_stmt import StmtVisitor
112
113
  from .visit_expr import ExprVisitor
113
114
  from .visit_call import CallVisitor
114
115
 
116
+ # DrawingVisitor owns the drawing-objects-as-data dispatch (line/box/label/
117
+ # linefill/chart.point lowering onto the per-type arenas) plus _uses_drawing
118
+ # detection and arena-cap computation. See codegen/drawing.py.
119
+ from .drawing import DrawingVisitor
120
+
115
121
 
116
122
  # ---------------------------------------------------------------------------
117
123
  # CodeGen class
118
124
  # ---------------------------------------------------------------------------
119
125
 
120
- class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, NamingHelper):
126
+ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, DrawingVisitor, NamingHelper):
121
127
  """Generate C++ from an AnalyzerContext (visitor pattern).
122
128
 
123
129
  Mixin chain (Python MRO is left-to-right; method names are
@@ -177,7 +183,22 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
177
183
  self._active_var_remap: dict[str, str] = {}
178
184
  # Set of var/series member names that belong to user functions (need cloning)
179
185
  self._func_var_members_set: set[str] = set()
186
+ # BUG C: function-local names emitted as ``UDT*`` pointer aliases (a UDT
187
+ # local initialised from a var/global UDT lvalue, mutated through, AND
188
+ # later rebound to a different lvalue). Member access lowers to ``->``
189
+ # and rebinds to ``&(...)``. Reset per function in _emit_func_def is not
190
+ # needed: names are function-unique and the value-copy fallback ignores
191
+ # entries for inactive functions.
192
+ self._udt_ptr_alias_locals: set[str] = set()
180
193
  self._precalc_loop_active: bool = False
194
+ # Names of ``var`` members that live in a FUNCTION scope (not global).
195
+ # These are initialized once-per-function-variant on first call (a
196
+ # function-local static equivalent), NOT in the constructor / on_bar
197
+ # preamble. See ``_emit_func_var_init_block``.
198
+ self._func_local_var_names: set[str] = set()
199
+ for _vlist in ctx.func_var_members.values():
200
+ for _n, _, _ in _vlist:
201
+ self._func_local_var_names.add(_n)
181
202
 
182
203
  # Build per-function var/series name lists for cloning.
183
204
  # For each function with call-site variants, collect ALL function-scoped
@@ -260,30 +281,61 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
260
281
 
261
282
  # Build cloned site remapping for cs > 0 (must happen before _ta_site_map
262
283
  # so cloned names are in _func_ta_members and get filtered out of the map)
284
+ #
285
+ # Default to the ``{orig}_cs{cs_idx}`` formula (matches the analyzer's clone
286
+ # naming), but defer to the analyzer's authoritative clone-name map for any
287
+ # site it had to disambiguate (a TA site reached through multiple enclosing
288
+ # functions would otherwise collide on the formula). Keeping the formula as
289
+ # the default leaves all non-colliding output byte-identical.
290
+ clone_names = getattr(ctx, "func_cs_ta_clone_names", {})
263
291
  for fname, orig_names in func_ta_originals.items():
264
292
  total_cs = ctx.func_call_site_counts.get(fname, 1)
265
293
  for cs_idx in range(1, total_cs):
294
+ overrides = clone_names.get((fname, cs_idx), {})
266
295
  remap = {}
267
296
  for orig_name in orig_names:
268
- remap[orig_name] = f"{orig_name}_cs{cs_idx}"
297
+ remap[orig_name] = overrides.get(
298
+ orig_name, f"{orig_name}_cs{cs_idx}")
269
299
  self._func_cs_ta_remap[(fname, cs_idx)] = remap
270
300
  self._func_ta_members.update(remap.values())
271
301
 
272
302
  for site in ctx.ta_call_sites:
273
303
  if site.node is not None:
274
304
  if site.member_name not in self._func_ta_members:
305
+ # Top-level (non-function) site: maps to itself.
275
306
  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
307
+ elif id(site.node) not in self._ta_site_map:
308
+ # Function-local site. Multiple clones share the SAME AST
309
+ # node (clones copy ``node=orig.node``); the FIRST one in
310
+ # ``ta_call_sites`` order is the canonical original (cs0)
311
+ # whose ``member_name`` the per-call-site remap is keyed on.
312
+ # Later clones (``_cs{i}``, ``_cs{i}_cs{j}``, ``_u{n}`` …)
313
+ # must NOT overwrite it: doing so poisons the base name so
314
+ # the active-remap lookup misses and every clone collapses
315
+ # onto one member. Keep the original; the variant member is
316
+ # resolved via ``_active_ta_remap`` at emit time.
278
317
  self._ta_site_map[id(site.node)] = site
279
318
  self._ta_index_by_site_id: dict[int, int] = {
280
319
  id(site): i for i, site in enumerate(ctx.ta_call_sites)
281
320
  }
321
+ # Context-sensitive (call-path) instance machinery for nested stateful
322
+ # helpers. Built by ``_build_func_instances`` below. ``_current_instance_name``
323
+ # names the function clone whose body is currently being emitted (None at
324
+ # top level / non-variant bodies). ``_instance_dispatch`` maps
325
+ # ``(enclosing_instance_name, call_node_id) -> callee emit-name`` and is the
326
+ # authority for nested stateful-helper dispatch (see visit_call).
327
+ self._current_instance_name: str | None = None
328
+ self._instance_dispatch: dict[tuple[str | None, int], str] = {}
329
+ self._fresh_instances: list[dict] = []
330
+ self._fresh_var_members: list[tuple[str, str]] = []
331
+ # NOTE: _build_func_instances() runs at the top of generate() (it needs
332
+ # _all_member_names / _func_safe_name, which are populated later in __init__).
282
333
  # Build lookup: node id -> FixnanCallSite (counter-based)
283
334
  self._fixnan_counter = 0
284
335
  self._switch_counter = 0
285
336
  self._security_inline_counter = 0
286
337
  self._random_call_counter = 0
338
+ self._for_counter = 0
287
339
  # UDT / enum (needed before _collect_known_vars for input.enum)
288
340
  self._udt_defs: dict[str, list] = {}
289
341
  self._enum_defs: dict[str, list[str]] = {}
@@ -308,6 +360,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
308
360
  # Map input-backed var name -> its input.*() FuncCall node so we can
309
361
  # later emit a runtime get_input_*() read with the same title/default.
310
362
  self._input_var_to_call: dict[str, FuncCall] = {}
363
+ # Class-scope arithmetic-over-input vars (e.g. ``wilderLen = rsiLen*2-1``).
364
+ # Maps the derived var name -> its raw RHS expression string. When such a
365
+ # var feeds a TA ctor length, the runtime-reset path expands it so input
366
+ # overrides propagate (``(get_input_int("RSI Length",14) * 2 - 1)``); the
367
+ # ctor-init list still folds to the Pine-default literal via _resolve_known.
368
+ self._derived_input_expr: dict[str, str] = {}
311
369
  self._timeframe_period_vars: set[str] = set()
312
370
  self._collect_known_vars()
313
371
  # Track var names
@@ -337,6 +395,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
337
395
  self._global_mutable_infos: dict[str, object] = getattr(ctx, "global_mutable_infos", {}) or {}
338
396
  self._udt_var_types: dict[str, str] = getattr(ctx, "udt_var_types", {}) or {}
339
397
  self._collection_types: dict[str, TypeSpec] = getattr(ctx, "collection_types", {}) or {}
398
+ # id(block_node) -> {raw_var_name: unique_member} for block-scoped var
399
+ # name collisions (see Analyzer._visit_VarDecl). Activated into
400
+ # ``_active_var_remap`` while emitting the owning block's statements.
401
+ self._block_var_renames: dict[int, dict[str, str]] = getattr(ctx, "block_var_renames", {}) or {}
340
402
  self._udt_field_type_specs: dict[str, dict[str, TypeSpec]] = getattr(ctx, "udt_field_type_specs", {}) or {}
341
403
  # Map UDT struct name -> set of field names that were dropped from the
342
404
  # emitted C++ struct because they had drawing-only types (label, line,
@@ -347,7 +409,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
347
409
  # rewrite or strip downstream references to those fields so the
348
410
  # generated C++ never references a member that doesn't exist on the
349
411
  # emitted struct. See: pineforge-codegen issue #10.
350
- _DRAWING_TYPES_INIT = {"label", "line", "box", "table", "linefill", "polyline", "chart.point"}
412
+ # Drawing-objects-as-data: line/box/label/linefill/chart.point are now
413
+ # REAL data (un-dropped from UDT structs). Only table/polyline stay
414
+ # dropped (no C++ representation). See drawing-objects-as-data.md §4.2.
415
+ _DRAWING_TYPES_INIT = {"table", "polyline"}
351
416
  self._udt_omitted_fields: dict[str, set[str]] = {}
352
417
  for _type_name, _fields in self._udt_defs.items():
353
418
  _omitted = set()
@@ -378,6 +443,29 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
378
443
  self._map_vars.add(_name)
379
444
  elif _spec.kind == "udt" and _spec.name:
380
445
  self._udt_var_types.setdefault(_name, _spec.name)
446
+ # Table / polyline variables and params have NO C++ representation
447
+ # (SKIP_VAR_TYPES). A *method* call on such a receiver
448
+ # (``panel.cell(...)``, ``dash.merge_cells(...)``) is a visual no-op
449
+ # that must be dropped — but unlike the namespace form
450
+ # (``table.cell(...)``) the receiver is a bare var/param the
451
+ # namespace-based skip cannot see. Collect those names so
452
+ # ``_is_skip_expr`` can drop their method calls.
453
+ _SKIP_DECL_TYPES = set(SKIP_VAR_TYPES) | {"polyline"}
454
+ self._visual_drop_vars: set[str] = set()
455
+ for _node in self._walk_ast(self.ctx.ast):
456
+ if isinstance(_node, VarDecl):
457
+ if _node.type_hint in _SKIP_DECL_TYPES:
458
+ self._visual_drop_vars.add(_node.name)
459
+ elif isinstance(_node.value, FuncCall):
460
+ _fn, _ns = self._resolve_callee(_node.value.callee)
461
+ if _fn == "new" and _ns in _SKIP_DECL_TYPES:
462
+ self._visual_drop_vars.add(_node.name)
463
+ elif isinstance(_node, (FuncDef, MethodDef)):
464
+ _hints = (getattr(_node, "annotations", None) or {}).get("param_type_hints") or []
465
+ for _i, _p in enumerate(getattr(_node, "params", []) or []):
466
+ _h = _hints[_i] if _i < len(_hints) else None
467
+ if _h and str(_h).replace(" ", "") in _SKIP_DECL_TYPES:
468
+ self._visual_drop_vars.add(_p)
381
469
  # Collect request.security metadata per call
382
470
  self._security_eval_info: list[dict] = []
383
471
  self._security_ta_variant_names: dict[tuple[int, int, tuple], str] = {}
@@ -388,15 +476,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
388
476
  lookahead_node = item.get("lookahead_node")
389
477
  ta_range = item.get("ta_range")
390
478
 
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
479
+ # Resolve the timeframe: a literal/const/global gives a static tf;
480
+ # a function-parameter tf is resolved from the call sites (the
481
+ # evaluator is a class method, so the param is not in scope there).
482
+ tf_str, tf_expr = self._resolve_security_tf(
483
+ tf_node, item.get("containing_func", ""))
400
484
 
401
485
  is_lookahead_on = False
402
486
  if lookahead_node is not None:
@@ -447,9 +531,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
447
531
  self._security_eval_info.append({
448
532
  "sec_id": sec_id,
449
533
  "tf": tf_str,
534
+ "tf_expr": tf_expr,
450
535
  "tf_node": tf_node,
451
536
  "gaps_on": is_gaps_on,
452
537
  "lookahead_on": is_lookahead_on,
538
+ "heikinashi": bool(item.get("heikinashi", False)),
453
539
  "ta_range": ta_range,
454
540
  "ta_indices": sorted(ta_indices),
455
541
  "ta_binding_stacks": ta_binding_stacks,
@@ -468,6 +554,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
468
554
 
469
555
  self._register_global_aggregate_member_types()
470
556
  self._uses_matrix = self._detect_matrix_usage()
557
+ # Drawing-objects-as-data: gate all new emission (drawing.hpp include +
558
+ # the per-type arenas) on this flag so non-drawing strategies stay
559
+ # byte-identical. Caps come from the strategy() header max_*_count.
560
+ self._uses_drawing = self._detect_drawing_usage()
561
+ self._drawing_caps = self._compute_drawing_caps() if self._uses_drawing else {}
471
562
 
472
563
  # max_bars_back: the per-variable history depth the engine's Series<T>
473
564
  # ring buffer should retain. Pine exposes this two ways — the
@@ -493,6 +584,194 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
493
584
  # history reads off security-helper series.
494
585
  self._max_bars_back_cap: int | None = self._compute_max_bars_back_cap()
495
586
 
587
+ # ------------------------------------------------------------------
588
+ # Context-sensitive (call-path) instance machinery
589
+ # ------------------------------------------------------------------
590
+ def _iter_func_calls(self, root) -> list:
591
+ """Collect every ``FuncCall`` node reachable from ``root`` (a stmt list
592
+ or single AST node). Order-independent; used by the instance pre-pass to
593
+ find nested user-function calls inside a function body."""
594
+ out: list = []
595
+ seen: set[int] = set()
596
+ stack: list = list(root) if isinstance(root, (list, tuple)) else [root]
597
+ while stack:
598
+ node = stack.pop()
599
+ if node is None:
600
+ continue
601
+ if isinstance(node, (list, tuple)):
602
+ stack.extend(node)
603
+ continue
604
+ if isinstance(node, dict):
605
+ stack.extend(node.values())
606
+ continue
607
+ if not hasattr(node, "__dict__"):
608
+ continue
609
+ nid = id(node)
610
+ if nid in seen:
611
+ continue
612
+ seen.add(nid)
613
+ if isinstance(node, FuncCall):
614
+ out.append(node)
615
+ for v in vars(node).values():
616
+ if isinstance(v, (list, tuple, dict)) or hasattr(v, "__dict__"):
617
+ stack.append(v)
618
+ return out
619
+
620
+ def _build_func_instances(self) -> None:
621
+ """Context-sensitive cloning of nested stateful helper functions.
622
+
623
+ A stateful helper ``G`` (carrying TA state and/or ``var`` members) may be
624
+ reached through several distinct call paths — e.g. ``leg`` called from
625
+ three clones of ``f_get`` (lengths 10/20/30) *and* directly. Each path is
626
+ a logically-distinct instance that must drive its OWN TA/var members.
627
+
628
+ The analyzer already mints the per-path members (via range-widening), but
629
+ the flat ``{G}_cs{idx}`` clone namespace conflates a callee's own textual
630
+ call sites with the enclosing function's call sites. This pre-pass walks
631
+ the call graph from each natural clone and, for every nested stateful
632
+ call, composes the enclosing clone's active remap with the callee's
633
+ per-call-site remap:
634
+
635
+ composed_ta[m] = R_enclosing.get(R_callee_cs[m], R_callee_cs[m])
636
+
637
+ When the composition equals the callee's natural ``cs{j}`` remap the call
638
+ dispatches to the existing ``{G}_cs{j}`` clone (output stays byte-identical
639
+ for the common single-caller case). Otherwise a fresh instance is minted,
640
+ bound to the path-specific members (and FRESH ``var`` members so two paths
641
+ never share scalar state). ``_instance_dispatch`` records the resolved
642
+ emit-name per ``(enclosing_instance, call_node)``; ``_fresh_instances`` /
643
+ ``_fresh_var_members`` carry the extra code to emit.
644
+ """
645
+ ctx = self.ctx
646
+ stateful = (set(ctx.func_ta_ranges.keys())
647
+ | set(ctx.func_var_members.keys())
648
+ | set(ctx.func_series_vars.keys()))
649
+ if not stateful:
650
+ return
651
+
652
+ func_bodies: dict[str, list] = {}
653
+ for fi in ctx.func_infos:
654
+ node = getattr(fi, "node", None)
655
+ if node is not None and getattr(node, "body", None):
656
+ func_bodies.setdefault(fi.name, node.body)
657
+
658
+ def ta_originals(fname: str) -> list[str]:
659
+ return list(self._func_cs_ta_remap.get((fname, 0), {}).keys())
660
+
661
+ def var_originals(fname: str) -> list[str]:
662
+ return [self._safe_name(n) for n, _, _ in ctx.func_var_members.get(fname, [])]
663
+
664
+ def natural_name(fname: str, cs_idx: int) -> str:
665
+ return f"{self._func_safe_name(fname)}_cs{cs_idx}"
666
+
667
+ interned: dict[tuple, dict] = {}
668
+ worklist: list[dict] = []
669
+ seen_walk: set[str] = set()
670
+ fresh_counter = 0
671
+
672
+ # Seed with the natural clones the flat emission loop produces.
673
+ for fname in stateful:
674
+ if fname not in func_bodies:
675
+ continue
676
+ total_cs = ctx.func_call_site_counts.get(fname, 0)
677
+ if total_cs > 0:
678
+ for k in range(total_cs):
679
+ worklist.append({
680
+ "fname": fname,
681
+ "name": natural_name(fname, k),
682
+ "ta_remap": self._func_cs_ta_remap.get((fname, k), {}),
683
+ "var_remap": self._func_cs_var_remap.get((fname, k), {}),
684
+ })
685
+ else:
686
+ worklist.append({
687
+ "fname": fname,
688
+ "name": self._func_safe_name(fname),
689
+ "ta_remap": {},
690
+ "var_remap": {},
691
+ })
692
+
693
+ while worklist:
694
+ inst = worklist.pop()
695
+ if inst["name"] in seen_walk:
696
+ continue
697
+ seen_walk.add(inst["name"])
698
+ body = func_bodies.get(inst["fname"])
699
+ if not body:
700
+ continue
701
+ active_ta = inst["ta_remap"]
702
+ for callnode in self._iter_func_calls(body):
703
+ cs_info = ctx.func_call_cs_map.get(id(callnode))
704
+ if cs_info is None:
705
+ continue
706
+ g_name, j = cs_info
707
+ if g_name not in stateful:
708
+ continue
709
+ natural_ta = self._func_cs_ta_remap.get((g_name, j), {})
710
+ composed_ta = {}
711
+ for m in ta_originals(g_name):
712
+ mid = natural_ta.get(m, m)
713
+ composed_ta[m] = active_ta.get(mid, mid)
714
+ if composed_ta == natural_ta:
715
+ # Path resolves to the callee's own cs{j} clone — reuse it.
716
+ self._instance_dispatch[(inst["name"], id(callnode))] = \
717
+ natural_name(g_name, j)
718
+ continue
719
+ key = (g_name, frozenset(composed_ta.items()))
720
+ ginst = interned.get(key)
721
+ if ginst is None:
722
+ fresh_counter += 1
723
+ inst_name = f"{self._func_safe_name(g_name)}__ni{fresh_counter}"
724
+ fvar_remap: dict[str, str] = {}
725
+ for v in var_originals(g_name):
726
+ fresh_member = f"{v}__ni{fresh_counter}"
727
+ fvar_remap[v] = fresh_member
728
+ self._fresh_var_members.append((v, fresh_member))
729
+ ginst = {
730
+ "fname": g_name,
731
+ "name": inst_name,
732
+ "ta_remap": composed_ta,
733
+ "var_remap": fvar_remap,
734
+ }
735
+ interned[key] = ginst
736
+ self._fresh_instances.append(ginst)
737
+ worklist.append(ginst)
738
+ self._instance_dispatch[(inst["name"], id(callnode))] = ginst["name"]
739
+
740
+ def _emit_cloned_var_decl(self, orig_safe: str, cloned_safe: str,
741
+ series_suffix: str, lines: list[str]) -> None:
742
+ """Declare a per-clone copy of a function-scoped ``var`` member, matching
743
+ the original's C++ type (series / matrix / array / map / drawing-handle /
744
+ UDT / scalar). Shared by the per-call-site clone loop and the fresh
745
+ context-sensitive instance loop."""
746
+ for vname, ptype, _init_str in self.ctx.var_members:
747
+ if self._safe_name(vname) == orig_safe:
748
+ cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
749
+ if vname in self.ctx.series_vars:
750
+ lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
751
+ elif vname in self._matrix_specs:
752
+ lines.append(f" {self._type_spec_to_cpp(self._matrix_specs[vname])} {cloned_safe};")
753
+ elif vname in self._array_vars:
754
+ lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(vname))} {cloned_safe};")
755
+ elif vname in self._map_vars:
756
+ lines.append(f" {self._type_spec_to_cpp(self._map_spec_for_name(vname))} {cloned_safe};")
757
+ elif vname in self._udt_var_types:
758
+ # Drawing handle / UDT var clone must match the original's
759
+ # type (Line/Label/Box/<UDT>), not the coarse PineType
760
+ # default (double) — otherwise the clone can't hold the
761
+ # handle and drawing access on it reads a garbage / na id.
762
+ udt_t = self._udt_var_types[vname]
763
+ handle_cpp = DRAWING_TYPE_TO_CPP.get(udt_t, udt_t)
764
+ lines.append(f" {handle_cpp} {cloned_safe} = {handle_cpp}{{}};")
765
+ else:
766
+ lines.append(f" {cpp_type} {cloned_safe};")
767
+ return
768
+ # Non-var series var
769
+ if orig_safe in [self._safe_name(n) for n in self.ctx.series_vars]:
770
+ cpp_type = self._series_type_for(orig_safe)
771
+ lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
772
+ else:
773
+ lines.append(f" double {cloned_safe} = 0.0;")
774
+
496
775
  @staticmethod
497
776
  def _int_literal_value(node: ASTNode | None) -> int | None:
498
777
  """Return the integer value of a (possibly unary-minus) NumberLiteral,
@@ -759,6 +1038,45 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
759
1038
  walk(stmt)
760
1039
  return reassigned
761
1040
 
1041
+ def _arith_expr_to_str(self, node) -> str | None:
1042
+ """Render a numeric arithmetic-over-identifiers expression to a string
1043
+ whose token spelling matches ``_resolve_known`` / ``_runtime_ctor_arg_for_reset``
1044
+ (e.g. ``rsiLen * 2 - 1``). Returns None for any node shape we don't fold
1045
+ (series subscripts, ternaries, etc.) so the caller leaves the var untracked.
1046
+ """
1047
+ if isinstance(node, NumberLiteral):
1048
+ v = node.value
1049
+ if isinstance(v, float) and v == int(v):
1050
+ return str(int(v))
1051
+ return str(v)
1052
+ if isinstance(node, Identifier):
1053
+ return node.name
1054
+ if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
1055
+ return f"{node.object.name}.{node.member}"
1056
+ if isinstance(node, BinOp):
1057
+ l = self._arith_expr_to_str(node.left)
1058
+ r = self._arith_expr_to_str(node.right)
1059
+ if l is None or r is None:
1060
+ return None
1061
+ return f"{l} {node.op} {r}"
1062
+ if isinstance(node, UnaryOp):
1063
+ o = self._arith_expr_to_str(node.operand)
1064
+ if o is None:
1065
+ return None
1066
+ return f"{node.op}{o}"
1067
+ if isinstance(node, FuncCall):
1068
+ callee = self._arith_expr_to_str(node.callee)
1069
+ if callee is None:
1070
+ return None
1071
+ parts = []
1072
+ for a in node.args:
1073
+ s = self._arith_expr_to_str(a)
1074
+ if s is None:
1075
+ return None
1076
+ parts.append(s)
1077
+ return f"{callee}({', '.join(parts)})"
1078
+ return None
1079
+
762
1080
  def _collect_known_var(self, node: VarDecl) -> None:
763
1081
  """Extract known constant value from a VarDecl."""
764
1082
  # Don't inline series variables — their values change over time
@@ -811,6 +1129,36 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
811
1129
  if stored:
812
1130
  self._input_backed_vars.add(node.name)
813
1131
  self._input_var_to_call[node.name] = node.value
1132
+ # Class-scope arithmetic over known/input-backed vars
1133
+ # (``wilderLen = rsiLen * 2 - 1``, ``n = math.round(len / 2)``).
1134
+ # Without this branch the derived name is untracked, the TA ctor arg
1135
+ # never folds, and the runtime-reset path silently degenerates to a
1136
+ # period of 1. We (a) fold to a literal for the ctor-init list and
1137
+ # (b) record the raw expression so the reset path can re-expand any
1138
+ # input-backed operand to its get_input_*() runtime read.
1139
+ elif isinstance(node.value, (BinOp, UnaryOp, FuncCall)):
1140
+ expr_str = self._arith_expr_to_str(node.value)
1141
+ if expr_str is not None:
1142
+ import re as _re
1143
+ tokens = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", expr_str))
1144
+ refs_known = any(t in self._known_vars for t in tokens)
1145
+ refs_input = any(t in self._input_backed_vars for t in tokens)
1146
+ refs_derived = any(t in self._derived_input_expr for t in tokens)
1147
+ if refs_known or refs_input or refs_derived:
1148
+ folded = self._resolve_known(expr_str)
1149
+ if self._is_compile_time_value(folded):
1150
+ try:
1151
+ num = float(folded)
1152
+ self._known_vars[node.name] = (
1153
+ int(num) if num == int(num) else num
1154
+ )
1155
+ except ValueError:
1156
+ pass
1157
+ if refs_input or refs_derived:
1158
+ # Track as input-backed so use-sites are not inlined and
1159
+ # the runtime-reset path emits the override-aware expr.
1160
+ self._derived_input_expr[node.name] = expr_str
1161
+ self._input_backed_vars.add(node.name)
814
1162
 
815
1163
  # ------------------------------------------------------------------
816
1164
  # Public entry point
@@ -869,9 +1217,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
869
1217
 
870
1218
  def generate(self) -> str:
871
1219
  """Generate C++ source from the AnalyzerContext."""
1220
+ # Context-sensitive instance pre-pass (needs the naming helpers populated
1221
+ # in __init__). Computes nested stateful-helper dispatch + fresh instances.
1222
+ self._build_func_instances()
872
1223
  # Pre-scan for strategy series vars
873
1224
  self._prescan_strategy_series()
874
1225
  self._security_ohlc_hist_fields_by_sec: dict[int, set[str]] = {}
1226
+ # request.security TA call-sites read at a history offset (``ta.ema(...)[k>=1]``).
1227
+ # Maps sec_id -> set of TA call-site indices needing an HTF history Series.
1228
+ self._security_ta_hist_idx_by_sec: dict[int, set[int]] = {}
875
1229
 
876
1230
  lines: list[str] = []
877
1231
 
@@ -897,6 +1251,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
897
1251
  continue
898
1252
  spec = field_specs.get(f.name) or self._type_spec_from_hint_name(f.type_name)
899
1253
  cpp_type = self._type_spec_to_cpp(spec)
1254
+ # Pine ``int`` is 64-bit (it routinely holds UNIX-ms timestamps
1255
+ # and large bar indices); emit UDT int fields as ``int64_t`` so a
1256
+ # field initialised from ``time``/``current_bar_.timestamp`` does
1257
+ # not truncate / narrow-init.
1258
+ if cpp_type == "int":
1259
+ cpp_type = "int64_t"
900
1260
  if f.default:
901
1261
  default = self._visit_expr(f.default)
902
1262
  else:
@@ -961,6 +1321,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
961
1321
  lines.append(
962
1322
  f" Series<double> {self._security_ohlc_hist_series_cpp(sec_id, field)}{_mbb};"
963
1323
  )
1324
+ self._security_ta_hist_idx_by_sec[sec_id] = (
1325
+ self._collect_security_ta_hist_indices(expr_node)
1326
+ )
1327
+ for name in self._security_ta_hist_series_names(sec_id):
1328
+ lines.append(f" Series<double> {name}{_mbb};")
964
1329
  continue
965
1330
  if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
966
1331
  hist_fields: set[str] = set()
@@ -982,6 +1347,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
982
1347
  lines.append(
983
1348
  f" Series<double> {self._security_ohlc_hist_series_cpp(sec_id, field)}{_mbb};"
984
1349
  )
1350
+ self._security_ta_hist_idx_by_sec[sec_id] = (
1351
+ self._collect_security_ta_hist_indices(expr_node)
1352
+ )
1353
+ for name in self._security_ta_hist_series_names(sec_id):
1354
+ lines.append(f" Series<double> {name}{_mbb};")
985
1355
 
986
1356
  if self._security_calls:
987
1357
  lines.append(' std::unordered_map<std::string, Series<double>> _security_helper_series_;')
@@ -1031,8 +1401,20 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1031
1401
  continue
1032
1402
  seen_var_members.add(name)
1033
1403
  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:
1404
+ # Detect array vars from init expression. Guard the substring
1405
+ # heuristic against a UDT constructor that merely WRAPS array.new /
1406
+ # array.from in its arguments — e.g.
1407
+ # ``var draw d = draw.new(array.new<line>(), array.new<line>())``
1408
+ # must declare as ``draw``, not ``std::vector<double>``. (Drawing
1409
+ # made this latent collision reachable.)
1410
+ _init_str_s = str(init_str)
1411
+ _is_udt_ctor_init = any(
1412
+ _init_str_s.startswith(f"{u}.new") for u in self._udt_defs
1413
+ )
1414
+ if (not _is_udt_ctor_init) and (
1415
+ "array.new" in _init_str_s or "array.from" in _init_str_s
1416
+ or name in self._array_vars
1417
+ ):
1036
1418
  self._array_vars.add(name)
1037
1419
  lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(name))} {safe};")
1038
1420
  continue
@@ -1063,6 +1445,17 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1063
1445
  # decl as ``double`` and the later ``z = SDZone{...}`` would not
1064
1446
  # compile (assigning SDZone to double).
1065
1447
  init_s = str(init_str)
1448
+ # Drawing handle var member (L-N2): a ``var line x`` declares as the
1449
+ # C++ handle struct (Series<Line> when also history-referenced).
1450
+ # Drawing names are NOT in _udt_defs, so the udt branch below would
1451
+ # self-zero them to double; handle them first.
1452
+ _draw_cpp = DRAWING_TYPE_TO_CPP.get(self._udt_var_types.get(name))
1453
+ if _draw_cpp is not None:
1454
+ if name in self.ctx.series_vars:
1455
+ lines.append(f" Series<{_draw_cpp}> {safe}{_mbb};")
1456
+ else:
1457
+ lines.append(f" {_draw_cpp} {safe};")
1458
+ continue
1066
1459
  udt_type = self._udt_var_types.get(name)
1067
1460
  if udt_type not in self._udt_defs:
1068
1461
  udt_type = None
@@ -1131,7 +1524,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1131
1524
  # data/validation/udt-method-probe-19-array-of-udt-method,
1132
1525
  # data/validation/udt-method-probe-20-udt-return-from-func.
1133
1526
  udt_t = self._udt_var_types[name]
1134
- lines.append(f" {udt_t} {safe} = {udt_t}{{}};")
1527
+ # Drawing handle global (L-N6 / U): map line/box/label/linefill
1528
+ # to the C++ handle struct (the default is na, id=-1).
1529
+ _draw_cpp = DRAWING_TYPE_TO_CPP.get(udt_t)
1530
+ if _draw_cpp is not None:
1531
+ lines.append(f" {_draw_cpp} {safe} = {_draw_cpp}{{}};")
1532
+ else:
1533
+ lines.append(f" {udt_t} {safe} = {udt_t}{{}};")
1135
1534
  else:
1136
1535
  expr = self.ctx.global_expr_map.get(name) if hasattr(self.ctx, "global_expr_map") else None
1137
1536
  cpp_type = self._infer_type(expr) if expr is not None else PINE_TYPE_TO_CPP.get(ptype, "double")
@@ -1148,37 +1547,55 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1148
1547
  if cloned_safe in emitted_clones:
1149
1548
  continue # already declared by another function's clone
1150
1549
  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;")
1550
+ self._emit_cloned_var_decl(orig_safe, cloned_safe, _mbb, lines)
1551
+
1552
+ # 8c2. Fresh var members for context-sensitive helper instances (nested
1553
+ # helpers reached through >1 distinct call path). Each fresh instance
1554
+ # gets its OWN scalar/series state so two paths never collide.
1555
+ for orig_safe, fresh_safe in self._fresh_var_members:
1556
+ if fresh_safe in emitted_clones:
1557
+ continue
1558
+ emitted_clones.add(fresh_safe)
1559
+ self._emit_cloned_var_decl(orig_safe, fresh_safe, _mbb, lines)
1560
+
1561
+ # 8d. Drawing-objects-as-data arenas (gated on _uses_drawing so
1562
+ # non-drawing strategies emit byte-identical C++). Each arena is a
1563
+ # per-strategy member -> reset-per-run is automatic. Caps come from
1564
+ # the strategy() header max_*_count (default 50; linefill default 50).
1565
+ if self._uses_drawing:
1566
+ caps = self._drawing_caps or {}
1567
+ lines.append(f" DrawingArena<LineRec> _pf_lines_{{{caps.get('line', 50)}}};")
1568
+ lines.append(f" DrawingArena<BoxRec> _pf_boxes_{{{caps.get('box', 50)}}};")
1569
+ lines.append(f" DrawingArena<LabelRec> _pf_labels_{{{caps.get('label', 50)}}};")
1570
+ lines.append(f" DrawingArena<LinefillRec> _pf_linefills_{{{caps.get('linefill', 50)}}};")
1177
1571
 
1178
1572
  # 9. _var_initialized flag
1179
1573
  if self.ctx.var_members:
1180
1574
  lines.append(" bool _var_initialized = false;")
1181
1575
 
1576
+ # 9a. Per-function-variant ``var`` init flags. A function-scoped
1577
+ # ``var`` (Pine "init once" semantics) is a function-local static:
1578
+ # its initializer runs on the FIRST call to that function variant
1579
+ # (with the first bar's values the function actually sees) and the
1580
+ # result persists for the strategy's lifetime. Each clone (cs0,
1581
+ # cs1, ...) is an independent instance with its own flag.
1582
+ # ``func_var_members`` is keyed by the plain Pine function name
1583
+ # (``fi.name``), so this matches both plain UDFs and UDT methods.
1584
+ for fi in self.ctx.func_infos:
1585
+ if fi.name not in self.ctx.func_var_members:
1586
+ continue
1587
+ total_cs = self.ctx.func_call_site_counts.get(fi.name, 0)
1588
+ if total_cs > 0:
1589
+ for cs_idx in range(total_cs):
1590
+ lines.append(f" bool _fvinit_{self._func_safe_name(fi.name)}_cs{cs_idx} = false;")
1591
+ else:
1592
+ lines.append(f" bool _fvinit_{self._func_safe_name(fi.name)} = false;")
1593
+
1594
+ # 9a2. ``var`` init flags for fresh context-sensitive helper instances.
1595
+ for inst in self._fresh_instances:
1596
+ if inst["fname"] in self.ctx.func_var_members and inst["var_remap"]:
1597
+ lines.append(f" bool _fvinit_{inst['name']} = false;")
1598
+
1182
1599
  # 9b. _ta_initialized_ flag for runtime TA re-sizing (first on_bar only).
1183
1600
  if self.ctx.ta_call_sites:
1184
1601
  lines.append(" bool _ta_initialized_ = false;")
@@ -1198,7 +1615,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1198
1615
  total_cs = self.ctx.func_call_site_counts.get(fi.name, 0)
1199
1616
  has_ta = fi.name in self.ctx.func_ta_ranges
1200
1617
  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:
1618
+ # A function whose ONLY reason to need per-call-site cloning is a
1619
+ # security-tf-monomorphized request.security (no TA/series state
1620
+ # of its own — see Analyzer._check_mixed_callsite_security_tf)
1621
+ # still needs N separate emitted bodies so self._active_call_site_idx
1622
+ # is set while each is emitted (read by the request.security
1623
+ # use-site lowering in visit_call.py to pick the right clone's
1624
+ # sec_id).
1625
+ needs_security_clone = fi.name in self.ctx.func_security_clone_only
1626
+ if (has_ta or has_series or needs_security_clone) and total_cs > 0:
1202
1627
  # Emit one variant per call site
1203
1628
  for cs_idx in range(total_cs):
1204
1629
  self._emit_func_def(fi, lines, call_site_idx=cs_idx)
@@ -1207,7 +1632,20 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1207
1632
  self._emit_func_def(fi, lines)
1208
1633
  lines.append("")
1209
1634
 
1635
+ # 10a. Fresh context-sensitive instances of nested stateful helpers
1636
+ # (reached through >1 distinct call path). Each is bound to its own
1637
+ # path-specific TA + var members; see _build_func_instances.
1638
+ if self._fresh_instances:
1639
+ fi_by_name = {fi.name: fi for fi in self.ctx.func_infos}
1640
+ for inst in self._fresh_instances:
1641
+ fi = fi_by_name.get(inst["fname"])
1642
+ if fi is None:
1643
+ continue
1644
+ self._emit_func_def(fi, lines, instance=inst)
1645
+ lines.append("")
1646
+
1210
1647
  # 11. on_bar()
1648
+ self._current_instance_name = None
1211
1649
  self._emit_on_bar(lines)
1212
1650
  lines.append("")
1213
1651
 
@@ -1319,6 +1757,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1319
1757
  def _is_skip_expr(self, node) -> bool:
1320
1758
  """Check if an expression should be skipped (visual/unsupported)."""
1321
1759
  if isinstance(node, FuncCall):
1760
+ # chart.point.* resolves to namespace "chart" (a SKIP_NAMESPACE) but
1761
+ # is REAL data (a ChartPoint aggregate literal). Never skip it.
1762
+ if self._is_chart_point_callee(node.callee):
1763
+ return False
1322
1764
  func_name, namespace = self._resolve_callee(node.callee)
1323
1765
  if func_name in SKIP_FUNC_NAMES:
1324
1766
  return True
@@ -1326,6 +1768,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1326
1768
  return True
1327
1769
  if namespace in SKIP_VAR_TYPES:
1328
1770
  return True
1771
+ # Method call on a table/polyline-typed receiver var/param
1772
+ # (``panel.cell(...)``). These types have no C++ representation, so
1773
+ # the call is a visual no-op — drop it (mirrors the namespace form).
1774
+ if namespace in self._visual_drop_vars:
1775
+ return True
1329
1776
  # strategy.risk.* — handled in _visit_stmt, not skipped
1330
1777
  if isinstance(node, MemberAccess):
1331
1778
  if isinstance(node.object, Identifier) and node.object.name in SKIP_NAMESPACES:
@@ -1373,6 +1820,24 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1373
1820
  """
1374
1821
  import re
1375
1822
  ident_re = re.compile(r"[A-Za-z_][A-Za-z_0-9]*")
1823
+
1824
+ # Expand class-scope derived vars (``wilderLen`` -> ``(rsiLen * 2 - 1)``)
1825
+ # to their raw RHS so the input-backed leaves become get_input_*() reads
1826
+ # below. Recursive (bounded) to handle chains of derived vars; guards
1827
+ # against cycles.
1828
+ def _expand_derived(s: str, seen: frozenset = frozenset(), depth: int = 0) -> str:
1829
+ if depth > 32:
1830
+ return s
1831
+ def _rep(m: re.Match) -> str:
1832
+ nm = m.group(0)
1833
+ if nm in self._derived_input_expr and nm not in seen:
1834
+ inner = self._derived_input_expr[nm]
1835
+ return "(" + _expand_derived(inner, seen | {nm}, depth + 1) + ")"
1836
+ return nm
1837
+ return ident_re.sub(_rep, s)
1838
+
1839
+ arg_str = _expand_derived(arg_str)
1840
+
1376
1841
  tokens = ident_re.findall(arg_str)
1377
1842
  input_tokens = [t for t in tokens if t in self._input_backed_vars]
1378
1843
  if not input_tokens: