@pineforge/codegen-pyodide 0.10.0 → 0.10.2

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.
@@ -6,6 +6,8 @@ results from AnalyzerContext instead of walking the AST to collect info.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ from dataclasses import dataclass
10
+
9
11
  from ..ast_nodes import (
10
12
  ASTNode, Program, StrategyDecl, VarDecl, Assignment, IfStmt, ForStmt, ForInStmt,
11
13
  WhileStmt, SwitchStmt, BreakStmt, ContinueStmt, FuncDef, ExprStmt,
@@ -23,10 +25,19 @@ from ..analyzer import (
23
25
  TA_NO_CTOR,
24
26
  TA_PERIOD_ARG,
25
27
  )
26
- from ..symbols import PineType, TypeSpec
28
+ from ..symbols import PineType, TypeSpec, method_receiver_type_name
27
29
  from .. import signatures as sigs
28
30
  from ..errors import CompileError, Diagnostic, Level, Phase, SourceLocation
29
31
 
32
+
33
+ @dataclass(frozen=True)
34
+ class _StableVarCtorLiteral:
35
+ """Exact declaration identity admitted by the TA-length literal route."""
36
+
37
+ value: int
38
+ declaration_id: int
39
+ top_level_index: int
40
+
30
41
  # ---------------------------------------------------------------------------
31
42
  # Mapping tables — definitions live in ``tables.py``; re-imported here so
32
43
  # inline references inside this module (BAR_FIELDS[name], MATH_FUNC_MAP[fn],
@@ -63,6 +74,7 @@ from .tables import (
63
74
  MATRIX_SORT_ALLOWED_GENERIC_ELEMS,
64
75
  MATH_FUNC_MAP,
65
76
  STR_FUNC_MAP,
77
+ _MATH_SIGN_CPP_FN,
66
78
  _merge_kwargs,
67
79
  )
68
80
 
@@ -95,7 +107,7 @@ from .helpers import CPP_RESERVED, NamingHelper
95
107
  from .types import TypeInferer
96
108
 
97
109
  # TaSiteHelper owns site lookup, .compute() arg construction, and the TA
98
- # hoisting machinery. The runtime-reset chain (_resolve_known and friends)
110
+ # call-site machinery. The runtime-reset chain (_resolve_known and friends)
99
111
  # stays on CodeGen for now because it relies on Python's compile-time
100
112
  # expression evaluator.
101
113
  from .ta import TaSiteHelper
@@ -154,7 +166,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
154
166
  per-function emitters used by Pine functions and UDT methods
155
167
  * ``SecurityEmitter`` -- ``request.security()`` lowering pipeline
156
168
  (evaluators, dispatch, rebind, TA variants)
157
- * ``TaSiteHelper`` -- TA call-site lookup + .compute() arg construction + TA hoisting
169
+ * ``TaSiteHelper`` -- TA call-site lookup + .compute() arg construction
158
170
  * ``TypeInferer`` -- _type_spec_*, _infer_type, _array/_map_method_expr
159
171
  * ``InputHelper`` -- Pine ``input.*`` defaults / titles / getter dispatch
160
172
  * ``NamingHelper`` -- _safe_name / _resolve_callee / _walk_ast / ...
@@ -172,6 +184,104 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
172
184
 
173
185
  def __init__(self, ctx: AnalyzerContext) -> None:
174
186
  self.ctx = ctx
187
+ # Lexical Pine names remain in ``ctx.func_var_members``. This overlay
188
+ # carries exact class-member identities only for collision-qualified
189
+ # ordinary FuncDefs (identity mappings for every other ordinary UDF).
190
+ self._func_var_storage_names: dict[str, dict[str, str]] = {
191
+ owner: dict(names)
192
+ for owner, names in getattr(
193
+ ctx, "func_var_storage_names", {}
194
+ ).items()
195
+ }
196
+ self._qualified_func_var_raw_names: set[str] = {
197
+ raw_name
198
+ for storage_names in self._func_var_storage_names.values()
199
+ for raw_name, storage_name in storage_names.items()
200
+ if storage_name != raw_name
201
+ }
202
+ self._ordinary_global_binding_names: set[str] = set(
203
+ getattr(ctx, "ordinary_global_binding_names", set())
204
+ )
205
+ self._ordinary_global_series_names: set[str] = set(
206
+ getattr(ctx, "ordinary_global_series_names", set())
207
+ )
208
+ self._nonpersistent_series_decl_names: set[str] = set(
209
+ getattr(ctx, "nonpersistent_series_decl_names", set())
210
+ )
211
+ self._func_nonpersistent_series_vars: dict[str, set[str]] = {
212
+ owner: set(names)
213
+ for owner, names in getattr(
214
+ ctx, "func_nonpersistent_series_vars", {}
215
+ ).items()
216
+ }
217
+ # Exact declarations that are direct children of Program.body. A
218
+ # same-named declaration inside a top-level ``if``/loop is still a
219
+ # lexical block local; raw membership in ``_global_member_vars`` must
220
+ # not route that nested declaration into the hoisted class member.
221
+ self._ordinary_global_var_decl_nodes: set[int] = {
222
+ id(stmt)
223
+ for stmt in ctx.ast.body
224
+ if isinstance(stmt, VarDecl)
225
+ and not stmt.is_var
226
+ and not stmt.is_varip
227
+ }
228
+ self._direct_program_var_decl_nodes: set[int] = {
229
+ id(stmt)
230
+ for stmt in ctx.ast.body
231
+ if isinstance(stmt, VarDecl)
232
+ }
233
+ self._direct_program_tuple_decl_nodes: set[int] = {
234
+ id(stmt)
235
+ for stmt in ctx.ast.body
236
+ if isinstance(stmt, TupleAssign)
237
+ }
238
+ self._direct_program_tuple_binding_names: set[str] = {
239
+ name
240
+ for stmt in ctx.ast.body
241
+ if isinstance(stmt, TupleAssign)
242
+ for name in stmt.names
243
+ if name != "_"
244
+ }
245
+ self._direct_program_binding_names: set[str] = {
246
+ stmt.name
247
+ for stmt in ctx.ast.body
248
+ if isinstance(stmt, VarDecl)
249
+ }
250
+ self._direct_program_binding_names.update(
251
+ name
252
+ for stmt in ctx.ast.body
253
+ if isinstance(stmt, TupleAssign)
254
+ for name in stmt.names
255
+ if name != "_"
256
+ )
257
+ self._callable_state_raw_names: set[str] = {
258
+ name
259
+ for members in ctx.func_var_members.values()
260
+ for name, _ptype, _init in members
261
+ }
262
+ self._callable_state_raw_names.update(
263
+ name
264
+ for names in ctx.func_series_vars.values()
265
+ for name in names
266
+ )
267
+
268
+ def func_var_storage(owner: str, raw_name: str) -> str:
269
+ return self._func_var_storage_names.get(owner, {}).get(
270
+ raw_name, raw_name
271
+ )
272
+
273
+ self._func_var_storage_name = func_var_storage
274
+ # Security metadata collection can render runtime timeframe
275
+ # expressions before the rest of the constructor state is prepared.
276
+ # Exact persistent-member identity is immutable analyzer output, so
277
+ # initialize it first for every expression visitor.
278
+ self._persistent_var_member_names: set[str] = {
279
+ self._safe_name(name) for name, _, _ in ctx.var_members
280
+ }
281
+ self._series_var_member_names: set[str] = {
282
+ self._safe_name(name)
283
+ for name in getattr(ctx, "series_var_members", set())
284
+ }
175
285
  # Build lookup: node id -> TACallSite (only for non-function-local sites)
176
286
  self._ta_site_map: dict[int, TACallSite] = {}
177
287
  # Build per-call-site TA member name remapping for user functions
@@ -179,8 +289,6 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
179
289
  self._func_cs_ta_remap: dict[tuple[str, int], dict[str, str]] = {}
180
290
  # Active TA name remap (set during per-call-site function emission)
181
291
  self._active_ta_remap: dict[str, str] = {}
182
- # Flag: inside a per-call-site function variant (enables TA hoisting)
183
- self._in_ta_func_variant: bool = False
184
292
  # Active call-site index (set during per-call-site function emission)
185
293
  self._active_call_site_idx: int | None = None
186
294
  # Set of TA member names that belong to user functions
@@ -200,32 +308,16 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
200
308
  self._reset_input_getter_mode: bool = False
201
309
  # Set of var/series member names that belong to user functions (need cloning)
202
310
  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()
220
311
  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``.
312
+ # Names of ``var`` members that live in a callable scope (not global).
313
+ # Their exact declaration statements own initialization; they must not
314
+ # be initialized by the constructor or the global on_bar preamble.
225
315
  self._func_local_var_names: set[str] = set()
226
- for _vlist in ctx.func_var_members.values():
316
+ for _owner, _vlist in ctx.func_var_members.items():
227
317
  for _n, _, _ in _vlist:
228
- self._func_local_var_names.add(_n)
318
+ self._func_local_var_names.add(
319
+ self._func_var_storage_name(_owner, _n)
320
+ )
229
321
 
230
322
  # Build per-function var/series name lists for cloning.
231
323
  # For each function with call-site variants, collect ALL function-scoped
@@ -241,17 +333,38 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
241
333
  # are themselves sets (analyzer stores ``dict[str, set]``), so we must
242
334
  # iterate each value in ``sorted`` order to be hash-seed independent.
243
335
  all_func_scoped_series: list[str] = []
244
- for svars in ctx.func_series_vars.values():
336
+ for owner, svars in ctx.func_series_vars.items():
245
337
  for sv in sorted(svars):
246
- if sv not in all_func_scoped_series:
247
- all_func_scoped_series.append(sv)
338
+ exact = self._func_var_storage_name(owner, sv)
339
+ # ``func_series_vars`` also carries history-referenced
340
+ # parameters/plain locals. Translate only a real persistent
341
+ # member whose analyzer-exact identity is Series storage.
342
+ if (self._safe_name(exact)
343
+ in self._series_var_member_names):
344
+ storage = exact
345
+ elif (sv in self._qualified_func_var_raw_names
346
+ and sv not in self._func_nonpersistent_series_vars.get(
347
+ owner, set()
348
+ )):
349
+ # A history parameter/plain local in an unrelated UDF may
350
+ # reuse the Pine spelling of owner-qualified persistent
351
+ # state. It has no class-member identity to propagate into
352
+ # every other function's clone map; doing so remaps an
353
+ # initializer's earlier global read to a bogus ``x_csN``
354
+ # Series member before the persistent local is active.
355
+ continue
356
+ else:
357
+ storage = sv
358
+ if storage not in all_func_scoped_series:
359
+ all_func_scoped_series.append(storage)
248
360
  # Also include function-scoped var_members (same ordered-list rationale).
249
361
  # ``ctx.func_var_members`` values are lists (already insertion-ordered).
250
362
  all_func_scoped_vars: list[str] = []
251
- for vlist in ctx.func_var_members.values():
363
+ for owner, vlist in ctx.func_var_members.items():
252
364
  for n, _, _ in vlist:
253
- if n not in all_func_scoped_vars:
254
- all_func_scoped_vars.append(n)
365
+ exact = self._func_var_storage_name(owner, n)
366
+ if exact not in all_func_scoped_vars:
367
+ all_func_scoped_vars.append(exact)
255
368
 
256
369
  # For each function with call-site cloning (has TA ranges or is called multiple times),
257
370
  # include ALL function-scoped series/var vars that could be used in its body.
@@ -265,13 +378,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
265
378
  # Include function's own vars
266
379
  if fname in ctx.func_var_members:
267
380
  for n, _, _ in ctx.func_var_members[fname]:
268
- if n not in orig_names:
269
- orig_names.append(n)
381
+ exact = self._func_var_storage_name(fname, n)
382
+ if exact not in orig_names:
383
+ orig_names.append(exact)
270
384
  # Include function's own series vars (set -> sorted for determinism)
271
385
  if fname in ctx.func_series_vars:
272
386
  for sv in sorted(ctx.func_series_vars[fname]):
273
- if sv not in orig_names:
274
- orig_names.append(sv)
387
+ exact = self._func_var_storage_name(fname, sv)
388
+ storage = (
389
+ exact
390
+ if self._safe_name(exact)
391
+ in self._series_var_member_names
392
+ else sv
393
+ )
394
+ if storage not in orig_names:
395
+ orig_names.append(storage)
275
396
  # Include series vars from sub-functions (they share the same class members)
276
397
  for sv in all_func_scoped_series:
277
398
  if sv not in orig_names:
@@ -300,7 +421,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
300
421
  # Build TA site map and per-call-site remapping
301
422
  func_ta_originals: dict[str, list[str]] = {} # func_name -> list of original member names
302
423
  for fname, (start, end) in ctx.func_ta_ranges.items():
303
- orig_names = [ctx.ta_call_sites[i].member_name for i in range(start, end)]
424
+ indices = (ctx.func_ta_indices or {}).get(fname)
425
+ if indices is None:
426
+ indices = range(start, end)
427
+ orig_names = [ctx.ta_call_sites[i].member_name for i in indices]
304
428
  func_ta_originals[fname] = orig_names
305
429
  self._func_ta_members.update(orig_names)
306
430
  # cs0 uses originals (identity mapping)
@@ -354,7 +478,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
354
478
  self._current_instance_name: str | None = None
355
479
  self._instance_dispatch: dict[tuple[str | None, int], str] = {}
356
480
  self._fresh_instances: list[dict] = []
357
- self._fresh_var_members: list[tuple[str, str]] = []
481
+ self._fresh_var_members: list[tuple[str, str, str]] = []
358
482
  # Fresh fixnan members for context-sensitive helper instances (nested
359
483
  # helpers reached through >1 distinct call path). Each fresh instance
360
484
  # gets its OWN previous-value member so two paths never share fixnan
@@ -381,6 +505,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
381
505
  self._security_inline_counter = 0
382
506
  self._random_call_counter = 0
383
507
  self._for_counter = 0
508
+ self._tuple_assign_counter = 0
384
509
  # Synthetic history buffers used by inline call-history and by scalar
385
510
  # expressions passed to UDF series parameters. They are pre-registered
386
511
  # at generate() time so declarations precede method emission, then
@@ -389,6 +514,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
389
514
  # the declaration-derived checkpoint inventory.
390
515
  self._inline_history_members: list[dict] = []
391
516
  self._inline_history_member_by_key: dict[tuple, str] = {}
517
+ # request.security/request.security_lower_tf inline their Pine helper
518
+ # expressions on the requested clock instead of calling the ordinary
519
+ # emitted UDF methods. The chart-clocked plain-UDF history factor must
520
+ # therefore exclude those source nodes and any emitted helper context
521
+ # reached only through them.
522
+ self._requested_context_inline_node_ids: set[int] = set()
523
+ self._requested_context_only_inline_contexts: set[
524
+ tuple[str, str | None]
525
+ ] = set()
392
526
  # Unique lambda-local names used when an array lowering references its
393
527
  # receiver more than once. The binding keeps temporary-producing or
394
528
  # side-effectful receivers single-evaluation (see TypeInferer).
@@ -408,6 +542,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
408
542
  self._current_input_var_name: str | None = None
409
543
  # Build known_vars for constant propagation
410
544
  self._known_vars: dict[str, int | float | bool | str] = {}
545
+ # Source-ordered lexical declarations can shadow a same-named direct
546
+ # compile-time global. Constant folding is raw-name keyed, so every
547
+ # fold path must consult this COW tombstone set before substituting a
548
+ # global literal inside the nested lexical scope.
549
+ self._lexical_known_var_tombstones: set[str] = set()
411
550
  # Subset of _known_vars whose value came from an input.*() call. These
412
551
  # MUST NOT be inlined at identifier use sites because strategy_set_input()
413
552
  # can override them at runtime. Ctor-time uses (TA buffer sizing,
@@ -433,6 +572,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
433
572
  # state are NOT here, so a TA length fed by them is still rejected
434
573
  # by the constructor guard.
435
574
  self._stable_runtime_vars: set[str] = set()
575
+ # Narrow subset of persistent scalars that a TA constructor may fold
576
+ # directly: one unique top-level ``var [int] name = <int literal>``
577
+ # binding, never reassigned and never promoted to Series storage.
578
+ # Keep this separate from ``_known_vars`` so ordinary identifier use
579
+ # still reads the persistent member and only TA buffer sizing changes.
580
+ self._stable_var_ctor_literals: dict[str, _StableVarCtorLiteral] = {}
436
581
  # ``_var_names`` (var/varip persistent-state members) is needed by the
437
582
  # stability classifier during _collect_known_vars, so pre-seed it from
438
583
  # the analyzer's var_members before that pass runs; the canonical
@@ -440,11 +585,30 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
440
585
  self._var_names: set[str] = set()
441
586
  for _vn, _, _ in ctx.var_members:
442
587
  self._var_names.add(_vn)
588
+ ordinary_global_member_names = self._ordinary_global_binding_names
589
+ for _vlist in ctx.func_var_members.values():
590
+ self._var_names.update(
591
+ _n
592
+ for _n, _, _ in _vlist
593
+ if _n not in ordinary_global_member_names
594
+ )
443
595
  self._collect_known_vars()
444
596
  # Track var names
445
597
  self._var_names = set()
446
598
  for name, _, _ in ctx.var_members:
447
599
  self._var_names.add(name)
600
+ # Preserve the established raw lexical allow-list even when the class
601
+ # member itself is owner-qualified. Declaration-site remapping resolves
602
+ # legal reads/writes to exact storage; this union also prevents the
603
+ # legacy non-var-Series loop from emitting a second raw class member.
604
+ # A real ordinary global with that spelling is deliberately excluded:
605
+ # it owns separate class state and must still be emitted.
606
+ for _vlist in ctx.func_var_members.values():
607
+ self._var_names.update(
608
+ _n
609
+ for _n, _, _ in _vlist
610
+ if _n not in ordinary_global_member_names
611
+ )
448
612
  # Every name bound ANYWHERE in the program (top-level, nested in
449
613
  # if/for/while/switch blocks, or inside function bodies). The
450
614
  # unknown-identifier guard in _visit_ident uses this as a generous
@@ -548,7 +712,33 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
548
712
  self._global_member_vars.add(name)
549
713
  self._global_mutable_infos: dict[str, object] = getattr(ctx, "global_mutable_infos", {}) or {}
550
714
  self._udt_var_types: dict[str, str] = getattr(ctx, "udt_var_types", {}) or {}
551
- self._collection_types: dict[str, TypeSpec] = getattr(ctx, "collection_types", {}) or {}
715
+ self._global_collection_types: dict[str, TypeSpec] = dict(
716
+ getattr(ctx, "collection_types", {}) or {}
717
+ )
718
+ self._collection_types: dict[str, TypeSpec] = dict(
719
+ self._global_collection_types
720
+ )
721
+ self._func_collection_types: dict[str, dict[str, TypeSpec]] = {
722
+ name: dict(specs)
723
+ for name, specs in (
724
+ getattr(ctx, "func_collection_types", {}) or {}
725
+ ).items()
726
+ }
727
+ self._block_collection_types: dict[int, dict[str, TypeSpec | None]] = {
728
+ owner_id: dict(specs)
729
+ for owner_id, specs in (
730
+ getattr(ctx, "block_collection_types", {}) or {}
731
+ ).items()
732
+ }
733
+ self._block_collection_owners: dict[int, str] = dict(
734
+ getattr(ctx, "block_collection_owners", {}) or {}
735
+ )
736
+ self._callable_collection_bindings: dict[int, TypeSpec | None] = dict(
737
+ getattr(ctx, "callable_collection_bindings", {}) or {}
738
+ )
739
+ self._callable_collection_binding_owners: dict[int, str] = dict(
740
+ getattr(ctx, "callable_collection_binding_owners", {}) or {}
741
+ )
552
742
  # id(block_node) -> {raw_var_name: unique_member} for block-scoped var
553
743
  # name collisions (see Analyzer._visit_VarDecl). Activated into
554
744
  # ``_active_var_remap`` while emitting the owning block's statements.
@@ -579,11 +769,44 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
579
769
  # Current function parameter types (set during _emit_func_def)
580
770
  self._current_func_param_types: dict[str, str] = {}
581
771
  self._current_func_param_specs: dict[str, "TypeSpec"] = {}
772
+ # Copy-on-write lexical collection inventory for the function/method
773
+ # currently being emitted. Activated by _emit_func_def and restored
774
+ # together with the legacy raw-name registries at function exit.
775
+ self._current_func_collection_specs: dict[str, "TypeSpec"] = {}
776
+ # Direct callable-body declarations that shadow a top-level collection.
777
+ # Nested declarations are activated only by their block COW overlay.
778
+ self._current_func_collection_shadows: set[str] = set()
779
+ # While emitting ``name = RHS`` for a declaration that shadows an
780
+ # already-active same-named collection, RHS identifiers are redirected
781
+ # through a pre-declaration alias. C++ brings the new name into scope
782
+ # inside its own initializer, so raw ``name`` would otherwise self-bind.
783
+ self._pending_decl_outer_alias: dict[str, str] = {}
784
+ self._collection_shadow_tmp_counter = 0
785
+ self._collection_shadow_tmp_names: set[str] = set()
582
786
  # Current function params that are series (const Series<double>&)
583
787
  self._current_func_series_params: set[str] = set()
788
+ # Element types parallel to ``_current_func_series_params``. Keeping
789
+ # these separate from the full C++ reference spelling lets expression
790
+ # inference see ``src`` as its Pine scalar family inside the body.
791
+ self._current_func_series_param_types: dict[str, str] = {}
584
792
  # Locals declared in the function currently being emitted (symbol table loses them after analysis)
585
793
  self._current_func_locals: set[str] = set()
586
794
  self._current_func_local_types: dict[str, str] = {}
795
+ # Source-ordered drawing-handle bindings in the currently emitted
796
+ # lexical block. Values are exact C++ handle types; ``None`` is a
797
+ # scalar/non-drawing tombstone that prevents an outer same-named handle
798
+ # from leaking inward. Block push/pop provides sibling isolation.
799
+ self._lexical_drawing_types: dict[str, str | None] = {}
800
+ # Source-ordered arbitrary UDT bindings. Drawing handles have their
801
+ # specialized registry above, while this parallel map preserves the
802
+ # exact authored UDT name needed to target-type a later bare ``na``
803
+ # reassignment. ``None`` is an ordinary scalar/collection tombstone;
804
+ # block push/pop keeps sibling declarations independent.
805
+ self._lexical_udt_types: dict[str, str | None] = {}
806
+ # Source-ordered lexical Series status. A False tombstone prevents a
807
+ # scalar local from inheriting a same-spelled global entry from the
808
+ # legacy raw-name ``ctx.series_vars`` union.
809
+ self._lexical_series_bindings: dict[str, bool] = {}
587
810
  # for-in loop iterator names (must resolve member access, not enum fallback)
588
811
  self._current_loop_vars: set[str] = set()
589
812
  self._current_loop_var_specs: dict[str, "TypeSpec"] = {}
@@ -708,9 +931,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
708
931
  self._all_member_names.add(self._safe_name(name))
709
932
  for name, _, _ in ctx.var_members:
710
933
  self._all_member_names.add(self._safe_name(name))
711
-
712
934
  self._register_global_aggregate_member_types()
713
- self._register_udt_array_get_ref_locals()
935
+ self._uses_map = self._detect_map_usage()
714
936
  self._uses_matrix = self._detect_matrix_usage()
715
937
  # Drawing-objects-as-data: gate all new emission (drawing.hpp include +
716
938
  # the per-type arenas) on this flag so non-drawing strategies stay
@@ -742,6 +964,328 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
742
964
  # history reads off security-helper series.
743
965
  self._max_bars_back_cap: int | None = self._compute_max_bars_back_cap()
744
966
 
967
+ # Declaration-site ``var`` metadata is prepared in ``generate()``
968
+ # after context-sensitive function instances have been built. Once
969
+ # flags share the class-member namespace with those fresh members, so
970
+ # allocating the flags here would not yet have the complete collision
971
+ # inventory.
972
+
973
+ def _scalar_var_init_depends_on_runtime_input(self, init_ast) -> bool:
974
+ """Whether a nominally constant initializer depends on input state.
975
+
976
+ ``_resolve_known`` intentionally folds input defaults for constructor
977
+ sizing and other compile-time decisions. A Pine ``var`` initializer,
978
+ however, must observe any host override installed before the first
979
+ bar. Treat direct input aliases and arithmetic aliases derived from
980
+ them as declaration-time expressions even when their default happens
981
+ to fold to a C++ literal.
982
+ """
983
+ if init_ast is None:
984
+ return False
985
+ runtime_names = set(self._input_backed_vars) | set(self._derived_input_expr)
986
+ for node in self._walk_ast(init_ast):
987
+ if isinstance(node, FuncCall) and self._is_input_call(node):
988
+ return True
989
+ if isinstance(node, Identifier) and node.name in runtime_names:
990
+ return True
991
+ return False
992
+
993
+ def _is_runtime_scalar_var_initializer(
994
+ self, name: str, ptype, init_str: str, init_ast,
995
+ drawing_cpp: str | None = None,
996
+ is_series: bool = False) -> bool:
997
+ """Return True for a persistent primitive that must init in execution.
998
+
999
+ Global Series and aggregate state keep their specialized preamble
1000
+ routes. Callable vars are selected separately by the exact-declaration
1001
+ rule below. This predicate identifies the remaining primitive global /
1002
+ on-bar-scope members that need declaration-site once guards.
1003
+ """
1004
+ if init_ast is None:
1005
+ return False
1006
+ if name in self._visual_drop_vars:
1007
+ return False
1008
+ if name in self._array_vars or name in self._map_vars \
1009
+ or name in self._matrix_specs:
1010
+ return False
1011
+ if drawing_cpp is not None:
1012
+ # A nontrivial drawing-handle initializer may depend on preceding
1013
+ # source-order state. Default construction already represents a
1014
+ # bare ``na``, so only real expressions need a declaration-site
1015
+ # once guard.
1016
+ return (
1017
+ is_series
1018
+ or not self._is_na_expr(init_ast)
1019
+ )
1020
+ if is_series and name in self.ctx.series_vars:
1021
+ return False
1022
+ udt_type = self._member_udt_type(name)
1023
+ if udt_type in self._udt_defs:
1024
+ return False
1025
+ type_spec = self._collection_types.get(name)
1026
+ if type_spec is not None and type_spec.kind in {
1027
+ "array", "map", "matrix", "udt",
1028
+ }:
1029
+ return False
1030
+
1031
+ ctor_val = self._resolve_known(init_str)
1032
+ ctor_val = self._typed_na_init(ctor_val, name, ptype)
1033
+ return (
1034
+ not self._is_compile_time_value(ctor_val)
1035
+ or self._scalar_var_init_depends_on_runtime_input(init_ast)
1036
+ )
1037
+
1038
+ def _prepare_runtime_scalar_var_initializers(self) -> None:
1039
+ """Index declaration-site scalar ``var`` initialization and flags.
1040
+
1041
+ The analyzer supplies exact metadata for every VarDecl, including
1042
+ sibling-block disambiguation and callable ownership. Its insertion
1043
+ order follows source analysis and reaches declarations nested inside
1044
+ if/switch expressions, while the ownership bit excludes function and
1045
+ method bodies. This lets emission preserve ordinary dependency order
1046
+ and Pine's lazy first-entry semantics for conditional declarations.
1047
+ """
1048
+ self._runtime_scalar_var_init_by_node: dict[int, dict] = {}
1049
+ self._runtime_scalar_var_init_by_member: dict[str, dict] = {}
1050
+ self._runtime_scalar_var_init_members: set[str] = set()
1051
+ self._drawing_var_member_cpp_types: dict[str, str] = {}
1052
+ self._drawing_var_decl_info_by_node: dict[int, dict] = {}
1053
+ self._global_drawing_cpp_types: dict[str, str] = {}
1054
+ # Exact direct-program UDT identity, including ``None`` tombstones for
1055
+ # primitive globals. The analyzer's legacy UDT registry is keyed only
1056
+ # by raw spelling and can be overwritten by an unrelated callable
1057
+ # local with the same name.
1058
+ self._global_udt_types: dict[str, str | None] = {}
1059
+ for stmt in self.ctx.ast.body:
1060
+ if not isinstance(stmt, VarDecl):
1061
+ continue
1062
+ spec = (
1063
+ self._type_spec_from_hint_name(stmt.type_hint)
1064
+ if stmt.type_hint
1065
+ else self._type_spec_from_expr(stmt.value)
1066
+ )
1067
+ self._global_udt_types[stmt.name] = (
1068
+ spec.name
1069
+ if spec is not None and spec.kind == "udt"
1070
+ else None
1071
+ )
1072
+ # Analyzer metadata preserves the collision-safe storage identity for
1073
+ # every persistent declaration, including chart-scope siblings that
1074
+ # are not direct Program children. Keep primitive tombstones too: the
1075
+ # raw-name UDT union must not swap ``state`` and ``state__blk1`` types.
1076
+ self._member_udt_types: dict[str, str | None] = {}
1077
+ metadata_by_node = getattr(
1078
+ self.ctx, "var_member_metadata_by_node", {}
1079
+ ) or {}
1080
+ type_specs_by_node = getattr(
1081
+ self.ctx, "var_member_type_specs_by_node", {}
1082
+ ) or {}
1083
+ for node_id, meta in metadata_by_node.items():
1084
+ stmt, member_name, _ptype, _init_str, _callable = meta
1085
+ spec = type_specs_by_node.get(node_id)
1086
+ if spec is None and isinstance(stmt, VarDecl):
1087
+ spec = (
1088
+ self._type_spec_from_hint_name(stmt.type_hint)
1089
+ if stmt.type_hint
1090
+ else self._type_spec_from_expr(stmt.value)
1091
+ )
1092
+ self._member_udt_types[member_name] = (
1093
+ spec.name
1094
+ if spec is not None and spec.kind == "udt"
1095
+ else None
1096
+ )
1097
+ self._runtime_var_init_flags: dict[tuple[int, str], str] = {}
1098
+
1099
+ used_names = set(self._all_member_names)
1100
+ # ``_all_member_names`` historically covers persistent ``var`` and
1101
+ # Series members only. Plain global declarations are class members as
1102
+ # well, so include them before minting a generated flag; otherwise a
1103
+ # legal user binding such as ``_pf_var_init_seeded = 1`` can collide
1104
+ # with the flag for ``var seeded = low``.
1105
+ used_names.update(
1106
+ self._safe_name(name) for name, _ptype in self.ctx.global_var_decls
1107
+ )
1108
+ # Natural call-site clones and context-sensitive fresh instances are
1109
+ # emitted later, but they occupy the same C++ class-member namespace as
1110
+ # these flags. Reserve the complete callable-state inventory up front;
1111
+ # in particular, an authored local named ``_pf_var_init_state`` also
1112
+ # produces a legal ``_pf_var_init_state_cs1`` storage clone.
1113
+ for remap in self._func_cs_var_remap.values():
1114
+ used_names.update(remap.values())
1115
+ used_names.update(
1116
+ fresh_safe
1117
+ for _owner, _orig_safe, fresh_safe in self._fresh_var_members
1118
+ )
1119
+ used_names.update(
1120
+ remapped
1121
+ for remap in self._func_cs_fixnan_remap.values()
1122
+ for remapped in remap.values()
1123
+ )
1124
+ used_names.update(
1125
+ fresh_safe for _site, fresh_safe in self._fresh_fixnan_members
1126
+ )
1127
+ # Keep function naming stable across this late preparation pass. A
1128
+ # flag must avoid an emitted base/clone/fresh method name rather than
1129
+ # making ``_func_safe_name`` change after the instance graph was built.
1130
+ for func_info in self.ctx.func_infos:
1131
+ emitted = self._func_cpp_base_name(func_info.name)
1132
+ used_names.add(emitted)
1133
+ for callsite in range(
1134
+ self.ctx.func_call_site_counts.get(func_info.name, 0)
1135
+ ):
1136
+ used_names.add(f"{emitted}_cs{callsite}")
1137
+ used_names.update(inst["name"] for inst in self._fresh_instances)
1138
+
1139
+ def allocate_flag(base: str) -> str:
1140
+ flag = base
1141
+ suffix = 2
1142
+ while flag in used_names:
1143
+ flag = f"{base}_{suffix}"
1144
+ suffix += 1
1145
+ used_names.add(flag)
1146
+ self._all_member_names.add(flag)
1147
+ return flag
1148
+
1149
+ metadata_by_node = getattr(
1150
+ self.ctx, "var_member_metadata_by_node", {}
1151
+ ) or {}
1152
+ type_specs_by_node = getattr(
1153
+ self.ctx, "var_member_type_specs_by_node", {}
1154
+ ) or {}
1155
+ owners_by_node = getattr(
1156
+ self.ctx, "var_member_owners_by_node", {}
1157
+ ) or {}
1158
+ top_level_node_ids = {id(stmt) for stmt in self.ctx.ast.body}
1159
+ for node_id, meta in metadata_by_node.items():
1160
+ stmt, member_name, ptype, init_str, is_callable_scoped = meta
1161
+ if not isinstance(stmt, VarDecl) or not (stmt.is_var or stmt.is_varip):
1162
+ continue
1163
+ stmt_spec = type_specs_by_node.get(node_id)
1164
+ if stmt_spec is None:
1165
+ stmt_spec = (
1166
+ self._type_spec_from_hint_name(stmt.type_hint)
1167
+ if stmt.type_hint
1168
+ else self._type_spec_from_expr(stmt.value)
1169
+ )
1170
+ drawing_cpp = (
1171
+ DRAWING_TYPE_TO_CPP.get(stmt_spec.name)
1172
+ if stmt_spec is not None and stmt_spec.kind == "udt"
1173
+ else None
1174
+ )
1175
+ if drawing_cpp is not None:
1176
+ self._drawing_var_member_cpp_types[member_name] = drawing_cpp
1177
+ self._drawing_var_decl_info_by_node[node_id] = {
1178
+ "node_id": node_id,
1179
+ "raw_name": stmt.name,
1180
+ "member_name": member_name,
1181
+ "drawing_cpp": drawing_cpp,
1182
+ "is_callable_scoped": is_callable_scoped,
1183
+ "owner": owners_by_node.get(node_id),
1184
+ "type_spec": stmt_spec,
1185
+ }
1186
+ if node_id in top_level_node_ids:
1187
+ self._global_drawing_cpp_types[stmt.name] = drawing_cpp
1188
+ is_series = self._safe_name(member_name) in self._series_var_member_names
1189
+ if node_id in self._drawing_var_decl_info_by_node:
1190
+ self._drawing_var_decl_info_by_node[node_id]["is_series"] = is_series
1191
+ # Every initialized callable ``var`` declaration must run at its
1192
+ # exact statement. Nested declarations may be unreached, while a
1193
+ # direct declaration can depend on ordinary statements immediately
1194
+ # before it; a function-entry preamble violates both Pine rules.
1195
+ # Exact-node once flags also preserve independent call-site clones.
1196
+ # A bare UDT/drawing ``na`` already has the correct default member
1197
+ # state and needs no assignment guard.
1198
+ callable_decl_site = (
1199
+ is_callable_scoped
1200
+ and stmt.value is not None
1201
+ and not (
1202
+ stmt_spec is not None
1203
+ and stmt_spec.kind == "udt"
1204
+ and self._is_na_expr(stmt.value)
1205
+ )
1206
+ )
1207
+ nullable_collection_selection_decl_site = (
1208
+ stmt_spec is not None
1209
+ and stmt_spec.kind in {"map", "matrix"}
1210
+ and isinstance(stmt.value, (IfStmt, SwitchStmt))
1211
+ )
1212
+ established_decl_site = (
1213
+ (not is_callable_scoped or drawing_cpp is not None)
1214
+ and (
1215
+ nullable_collection_selection_decl_site
1216
+ or self._is_runtime_scalar_var_initializer(
1217
+ member_name, ptype, init_str, stmt.value, drawing_cpp,
1218
+ is_series
1219
+ )
1220
+ )
1221
+ )
1222
+ if not callable_decl_site and not established_decl_site:
1223
+ continue
1224
+
1225
+ base_flag = f"_pf_var_init_{self._safe_name(member_name)}"
1226
+ flag = allocate_flag(base_flag)
1227
+ self._runtime_scalar_var_init_members.add(member_name)
1228
+ info = {
1229
+ "member_name": member_name,
1230
+ "node_id": node_id,
1231
+ "owner": owners_by_node.get(node_id),
1232
+ "is_callable_scoped": is_callable_scoped,
1233
+ "ptype": ptype,
1234
+ "flag": flag,
1235
+ "drawing_cpp": drawing_cpp,
1236
+ "type_spec": stmt_spec,
1237
+ "is_series": is_series,
1238
+ }
1239
+ self._runtime_scalar_var_init_by_node[node_id] = info
1240
+ base_storage = self._safe_name(member_name)
1241
+ self._runtime_var_init_flags[(node_id, base_storage)] = flag
1242
+ if is_callable_scoped:
1243
+ owner = owners_by_node.get(node_id)
1244
+ for (remap_owner, _cs_idx), remap in self._func_cs_var_remap.items():
1245
+ if remap_owner != owner or base_storage not in remap:
1246
+ continue
1247
+ storage = remap[base_storage]
1248
+ if storage == base_storage:
1249
+ continue
1250
+ clone_base = f"_pf_var_init_{storage}"
1251
+ clone_flag = allocate_flag(clone_base)
1252
+ self._runtime_var_init_flags[(node_id, storage)] = clone_flag
1253
+ for inst in self._fresh_instances:
1254
+ if inst.get("fname") != owner:
1255
+ continue
1256
+ storage = inst.get("var_remap", {}).get(
1257
+ base_storage, base_storage
1258
+ )
1259
+ key = (node_id, storage)
1260
+ if key in self._runtime_var_init_flags:
1261
+ continue
1262
+ fresh_base = f"_pf_var_init_{storage}"
1263
+ self._runtime_var_init_flags[key] = allocate_flag(
1264
+ fresh_base
1265
+ )
1266
+ if not is_callable_scoped:
1267
+ self._runtime_scalar_var_init_by_member[member_name] = info
1268
+
1269
+ self._runtime_var_init_flag_used_names = used_names
1270
+
1271
+ # Top-level non-persistent drawing declarations are not represented in
1272
+ # var-member metadata. Record their exact source declaration type so
1273
+ # functions can resolve globals without consulting the analyzer's
1274
+ # legacy raw-name UDT registry (which later locals may overwrite).
1275
+ for stmt in self.ctx.ast.body:
1276
+ if not isinstance(stmt, VarDecl) or stmt.is_var or stmt.is_varip:
1277
+ continue
1278
+ spec = (
1279
+ self._type_spec_from_hint_name(stmt.type_hint)
1280
+ if stmt.type_hint
1281
+ else self._type_spec_from_expr(stmt.value)
1282
+ )
1283
+ if (spec is not None and spec.kind == "udt"
1284
+ and spec.name in DRAWING_TYPE_TO_CPP):
1285
+ self._global_drawing_cpp_types[stmt.name] = (
1286
+ DRAWING_TYPE_TO_CPP[spec.name]
1287
+ )
1288
+
745
1289
  # ------------------------------------------------------------------
746
1290
  # Context-sensitive (call-path) instance machinery
747
1291
  # ------------------------------------------------------------------
@@ -815,11 +1359,69 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
815
1359
  if node is not None and getattr(node, "body", None):
816
1360
  func_bodies.setdefault(fi.name, node.body)
817
1361
 
1362
+ # Pine forbids recursive callable execution. Most scalar-only cycles
1363
+ # never enter this state-instance pass (and some legacy dead-branch
1364
+ # typing probes intentionally retain them), but a cycle carrying TA,
1365
+ # var, series, fixnan, or transitive state would otherwise mint a new
1366
+ # fresh path instance forever. Reject exactly that stateful subgraph
1367
+ # before expanding any members or dispatch records.
1368
+ stateful_edges: dict[str, list[tuple[str, FuncCall]]] = {
1369
+ name: [] for name in stateful
1370
+ }
1371
+ inherited_names = getattr(
1372
+ ctx, "func_inherited_call_names", {}
1373
+ ) or {}
1374
+ for owner, body in func_bodies.items():
1375
+ if owner not in stateful:
1376
+ continue
1377
+ for callnode in self._iter_func_calls(body):
1378
+ cs_info = ctx.func_call_cs_map.get(id(callnode))
1379
+ callee = (
1380
+ cs_info[0]
1381
+ if cs_info is not None
1382
+ else inherited_names.get(id(callnode))
1383
+ )
1384
+ if callee in stateful:
1385
+ stateful_edges[owner].append((callee, callnode))
1386
+
1387
+ colors: dict[str, int] = {}
1388
+ stack: list[str] = []
1389
+
1390
+ def reject_stateful_cycle(name: str) -> None:
1391
+ colors[name] = 1
1392
+ stack.append(name)
1393
+ for callee, callnode in stateful_edges.get(name, ()):
1394
+ color = colors.get(callee, 0)
1395
+ if color == 1:
1396
+ start = stack.index(callee)
1397
+ cycle = [*stack[start:], callee]
1398
+ self._codegen_error(
1399
+ callnode,
1400
+ "Recursive stateful callable cycle is not supported: "
1401
+ + " -> ".join(cycle)
1402
+ + ".",
1403
+ hint=(
1404
+ "Remove the recursive call; Pine user-defined "
1405
+ "functions cannot execute recursively."
1406
+ ),
1407
+ )
1408
+ if color == 0:
1409
+ reject_stateful_cycle(callee)
1410
+ stack.pop()
1411
+ colors[name] = 2
1412
+
1413
+ for name in sorted(stateful):
1414
+ if colors.get(name, 0) == 0:
1415
+ reject_stateful_cycle(name)
1416
+
818
1417
  def ta_originals(fname: str) -> list[str]:
819
1418
  return list(self._func_cs_ta_remap.get((fname, 0), {}).keys())
820
1419
 
821
1420
  def var_originals(fname: str) -> list[str]:
822
- return [self._safe_name(n) for n, _, _ in ctx.func_var_members.get(fname, [])]
1421
+ return [
1422
+ self._safe_name(self._func_var_storage_name(fname, n))
1423
+ for n, _, _ in ctx.func_var_members.get(fname, [])
1424
+ ]
823
1425
 
824
1426
  def fixnan_originals(fname: str) -> list[str]:
825
1427
  return list(self._func_cs_fixnan_remap.get((fname, 0), {}).keys())
@@ -842,6 +1444,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
842
1444
  worklist.append({
843
1445
  "fname": fname,
844
1446
  "name": natural_name(fname, k),
1447
+ "call_site_idx": k,
845
1448
  "ta_remap": self._func_cs_ta_remap.get((fname, k), {}),
846
1449
  "var_remap": self._func_cs_var_remap.get((fname, k), {}),
847
1450
  "fixnan_remap": self._func_cs_fixnan_remap.get((fname, k), {}),
@@ -850,6 +1453,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
850
1453
  worklist.append({
851
1454
  "fname": fname,
852
1455
  "name": self._func_cpp_base_name(fname),
1456
+ "call_site_idx": None,
853
1457
  "ta_remap": {},
854
1458
  "var_remap": {},
855
1459
  "fixnan_remap": {},
@@ -864,12 +1468,24 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
864
1468
  if not body:
865
1469
  continue
866
1470
  active_ta = inst["ta_remap"]
1471
+ active_var = inst.get("var_remap", {})
867
1472
  active_fixnan = inst.get("fixnan_remap", {})
868
1473
  for callnode in self._iter_func_calls(body):
869
1474
  cs_info = ctx.func_call_cs_map.get(id(callnode))
870
1475
  if cs_info is None:
871
- continue
872
- g_name, j = cs_info
1476
+ # Natural csN parents deliberately leave inherited nested
1477
+ # calls unmapped so visit_call can thread N through its
1478
+ # active-index fallback. A fresh context-sensitive parent
1479
+ # has no active index; recover only that removed edge's
1480
+ # exact callee identity and compose it from natural cs0.
1481
+ if not inst.get("fresh", False):
1482
+ continue
1483
+ g_name = ctx.func_inherited_call_names.get(id(callnode))
1484
+ if g_name is None:
1485
+ continue
1486
+ j = 0
1487
+ else:
1488
+ g_name, j = cs_info
873
1489
  if g_name not in stateful:
874
1490
  continue
875
1491
  natural_ta = self._func_cs_ta_remap.get((g_name, j), {})
@@ -877,18 +1493,56 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
877
1493
  for m in ta_originals(g_name):
878
1494
  mid = natural_ta.get(m, m)
879
1495
  composed_ta[m] = active_ta.get(mid, mid)
1496
+ g_var_originals = var_originals(g_name)
1497
+ full_natural_var = self._func_cs_var_remap.get(
1498
+ (g_name, j), {}
1499
+ )
1500
+ natural_var = {
1501
+ name: full_natural_var.get(name, name)
1502
+ for name in g_var_originals
1503
+ }
1504
+ composed_var = {}
1505
+ for m in g_var_originals:
1506
+ mid = natural_var.get(m, m)
1507
+ composed_var[m] = active_var.get(mid, mid)
880
1508
  natural_fixnan = self._func_cs_fixnan_remap.get((g_name, j), {})
881
1509
  composed_fixnan = {}
882
1510
  for m in fixnan_originals(g_name):
883
1511
  mid = natural_fixnan.get(m, m)
884
1512
  composed_fixnan[m] = active_fixnan.get(mid, mid)
885
- if composed_ta == natural_ta and composed_fixnan == natural_fixnan:
1513
+ owns_non_ta_state = bool(
1514
+ g_var_originals or fixnan_originals(g_name)
1515
+ )
1516
+ # Every non-cs0 enclosing variant is a distinct Pine written
1517
+ # call path. Even a pure wrapper can reach var/fixnan state
1518
+ # farther down the graph, so preserve the path identity here
1519
+ # and let the fresh wrapper compose its next edge in turn.
1520
+ path_requires_fresh_state = (
1521
+ inst.get("fresh", False)
1522
+ or inst.get("call_site_idx") not in (None, 0)
1523
+ )
1524
+ if (
1525
+ not path_requires_fresh_state
1526
+ and composed_ta == natural_ta
1527
+ and composed_var == natural_var
1528
+ and composed_fixnan == natural_fixnan
1529
+ ):
886
1530
  # Path resolves to the callee's own cs{j} clone — reuse it.
887
1531
  self._instance_dispatch[(inst["name"], id(callnode))] = \
888
1532
  natural_name(g_name, j)
889
1533
  continue
890
- key = (g_name, frozenset(composed_ta.items()),
891
- frozenset(composed_fixnan.items()))
1534
+ path_identity = (
1535
+ (inst["name"], id(callnode))
1536
+ if owns_non_ta_state or path_requires_fresh_state
1537
+ else None
1538
+ )
1539
+ key = (
1540
+ g_name,
1541
+ frozenset(composed_ta.items()),
1542
+ frozenset(composed_var.items()),
1543
+ frozenset(composed_fixnan.items()),
1544
+ path_identity,
1545
+ )
892
1546
  ginst = interned.get(key)
893
1547
  if ginst is None:
894
1548
  fresh_counter += 1
@@ -897,7 +1551,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
897
1551
  for v in var_originals(g_name):
898
1552
  fresh_member = f"{v}__ni{fresh_counter}"
899
1553
  fvar_remap[v] = fresh_member
900
- self._fresh_var_members.append((v, fresh_member))
1554
+ self._fresh_var_members.append((g_name, v, fresh_member))
901
1555
  # Fresh fixnan members: each path gets its OWN previous-
902
1556
  # value member so two call paths never share fixnan state.
903
1557
  ffixnan_remap: dict[str, str] = {}
@@ -917,6 +1571,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
917
1571
  ginst = {
918
1572
  "fname": g_name,
919
1573
  "name": inst_name,
1574
+ "fresh": True,
1575
+ "call_site_idx": None,
920
1576
  "ta_remap": composed_ta,
921
1577
  "var_remap": fvar_remap,
922
1578
  "fixnan_remap": ffixnan_remap,
@@ -926,8 +1582,107 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
926
1582
  worklist.append(ginst)
927
1583
  self._instance_dispatch[(inst["name"], id(callnode))] = ginst["name"]
928
1584
 
1585
+ def _callable_var_collection_spec(
1586
+ self, name: str, owner_func: str | None = None) -> TypeSpec | None:
1587
+ """Exact TypeSpec for a function-scoped persistent collection member.
1588
+
1589
+ Only declaration nodes that actually became callable-scoped persistent
1590
+ members are eligible here. A raw-name scan of the general callable
1591
+ inventory can otherwise turn a global scalar member into a collection
1592
+ merely because an unrelated helper has a same-named ordinary local.
1593
+ Clone contexts may own state reachable through another callable, so an
1594
+ explicit owner is preferred and the owner-less fallback remains
1595
+ deliberately limited to a single unambiguous persistent TypeSpec.
1596
+ """
1597
+ safe = self._safe_name(name)
1598
+
1599
+ candidates_by_owner: dict[str, list[TypeSpec]] = {}
1600
+ metadata = getattr(self.ctx, "var_member_metadata_by_node", {}) or {}
1601
+ for node_id, meta in metadata.items():
1602
+ _node, member_name, _ptype, _init_str, is_callable_scoped = meta
1603
+ if (not is_callable_scoped
1604
+ or self._safe_name(member_name) != safe):
1605
+ continue
1606
+ spec = self._callable_collection_bindings.get(node_id)
1607
+ owner = self._callable_collection_binding_owners.get(node_id)
1608
+ if (owner is None or spec is None
1609
+ or spec.kind not in {"array", "map", "matrix"}):
1610
+ continue
1611
+ owner_candidates = candidates_by_owner.setdefault(owner, [])
1612
+ if spec not in owner_candidates:
1613
+ owner_candidates.append(spec)
1614
+
1615
+ if owner_func is not None:
1616
+ owned = candidates_by_owner.get(owner_func, [])
1617
+ if len(owned) == 1:
1618
+ return owned[0]
1619
+ # The existing clone inventory deliberately unions persistent vars
1620
+ # from reachable helpers into an enclosing function's remap. In
1621
+ # that case ``owner_func`` names the clone context, not the lexical
1622
+ # declaration owner; fall through to the unambiguous owner search.
1623
+
1624
+ candidates: list[TypeSpec] = []
1625
+ for owned in candidates_by_owner.values():
1626
+ for spec in owned:
1627
+ if spec not in candidates:
1628
+ candidates.append(spec)
1629
+ return candidates[0] if len(candidates) == 1 else None
1630
+
1631
+ def _callable_var_udt_spec(
1632
+ self, name: str, owner_func: str | None = None) -> TypeSpec | None:
1633
+ """Exact UDT TypeSpec for one callable persistent member identity.
1634
+
1635
+ The analyzer's legacy ``_udt_var_types`` registry is keyed by raw Pine
1636
+ spelling, so sibling declarations such as ``state`` / ``state__blk1``
1637
+ can overwrite each other. Declaration metadata retains both the exact
1638
+ collision-safe member name and lexical owner; use it for base members
1639
+ and every written-callsite clone.
1640
+ """
1641
+ safe = self._safe_name(name)
1642
+ candidates_by_owner: dict[str, list[TypeSpec]] = {}
1643
+ metadata = getattr(self.ctx, "var_member_metadata_by_node", {}) or {}
1644
+ specs = getattr(self.ctx, "var_member_type_specs_by_node", {}) or {}
1645
+ owners = getattr(self.ctx, "var_member_owners_by_node", {}) or {}
1646
+ for node_id, meta in metadata.items():
1647
+ _node, member_name, _ptype, _init_str, is_callable_scoped = meta
1648
+ if (not is_callable_scoped
1649
+ or self._safe_name(member_name) != safe):
1650
+ continue
1651
+ spec = specs.get(node_id)
1652
+ owner = owners.get(node_id)
1653
+ if (owner is None or spec is None
1654
+ or spec.kind != "udt"
1655
+ or spec.name not in self._udt_defs):
1656
+ continue
1657
+ owner_candidates = candidates_by_owner.setdefault(owner, [])
1658
+ if spec not in owner_candidates:
1659
+ owner_candidates.append(spec)
1660
+
1661
+ if owner_func is not None:
1662
+ owned = candidates_by_owner.get(owner_func, [])
1663
+ if len(owned) == 1:
1664
+ return owned[0]
1665
+
1666
+ candidates: list[TypeSpec] = []
1667
+ for owned in candidates_by_owner.values():
1668
+ for spec in owned:
1669
+ if spec not in candidates:
1670
+ candidates.append(spec)
1671
+ return candidates[0] if len(candidates) == 1 else None
1672
+
1673
+ def _member_udt_type(self, name: str) -> str | None:
1674
+ """Exact UDT type for class-member storage, with global tombstones."""
1675
+ global_types = getattr(self, "_global_udt_types", {})
1676
+ if name in global_types:
1677
+ return global_types[name]
1678
+ member_types = getattr(self, "_member_udt_types", {})
1679
+ if name in member_types:
1680
+ return member_types[name]
1681
+ return self._udt_var_types.get(name)
1682
+
929
1683
  def _emit_cloned_var_decl(self, orig_safe: str, cloned_safe: str,
930
- series_suffix: str, lines: list[str]) -> None:
1684
+ series_suffix: str, lines: list[str],
1685
+ owner_func: str | None = None) -> None:
931
1686
  """Declare a per-clone copy of a function-scoped ``var`` member, matching
932
1687
  the original's C++ type (series / matrix / array / map / drawing-handle /
933
1688
  UDT / scalar). Shared by the per-call-site clone loop and the fresh
@@ -935,22 +1690,59 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
935
1690
  for vname, ptype, _init_str in self.ctx.var_members:
936
1691
  if self._safe_name(vname) == orig_safe:
937
1692
  cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
938
- if vname in self.ctx.series_vars:
1693
+ if cpp_type == "int" and self._is_int64_builtin_init(vname):
1694
+ cpp_type = "int64_t"
1695
+ collection_spec = self._callable_var_collection_spec(
1696
+ vname, owner_func
1697
+ )
1698
+ udt_spec = self._callable_var_udt_spec(vname, owner_func)
1699
+ member_udt_type = self._member_udt_type(vname)
1700
+ drawing_cpp = self._drawing_var_member_cpp_types.get(vname)
1701
+ if (drawing_cpp is not None
1702
+ and orig_safe in self._series_var_member_names):
1703
+ lines.append(
1704
+ f" Series<{drawing_cpp}> {cloned_safe}{series_suffix};"
1705
+ )
1706
+ elif (orig_safe in self._series_var_member_names
1707
+ or vname in self.ctx.series_vars):
939
1708
  lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
1709
+ elif collection_spec is not None:
1710
+ lines.append(
1711
+ f" {self._type_spec_to_cpp(collection_spec)} {cloned_safe};"
1712
+ )
940
1713
  elif vname in self._matrix_specs:
941
1714
  lines.append(f" {self._type_spec_to_cpp(self._matrix_specs[vname])} {cloned_safe};")
942
1715
  elif vname in self._array_vars:
943
1716
  lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(vname))} {cloned_safe};")
944
1717
  elif vname in self._map_vars:
945
1718
  lines.append(f" {self._type_spec_to_cpp(self._map_spec_for_name(vname))} {cloned_safe};")
946
- elif vname in self._udt_var_types:
1719
+ elif drawing_cpp is not None:
1720
+ lines.append(
1721
+ f" {drawing_cpp} {cloned_safe} = {drawing_cpp}{{}};"
1722
+ )
1723
+ elif udt_spec is not None or member_udt_type is not None:
947
1724
  # Drawing handle / UDT var clone must match the original's
948
1725
  # type (Line/Label/Box/<UDT>), not the coarse PineType
949
1726
  # default (double) — otherwise the clone can't hold the
950
1727
  # handle and drawing access on it reads a garbage / na id.
951
- udt_t = self._udt_var_types[vname]
1728
+ udt_t = (
1729
+ udt_spec.name
1730
+ if udt_spec is not None
1731
+ else member_udt_type
1732
+ )
952
1733
  handle_cpp = DRAWING_TYPE_TO_CPP.get(udt_t, udt_t)
953
1734
  lines.append(f" {handle_cpp} {cloned_safe} = {handle_cpp}{{}};")
1735
+ elif vname in self._runtime_scalar_var_init_members:
1736
+ # Declaration-site state may remain unreached while COOF
1737
+ # snapshots value-copy every clone. Match the base member's
1738
+ # typed Pine-na default instead of leaving primitive clone
1739
+ # storage indeterminate (or giving it a legacy zero).
1740
+ pending = self._typed_na_init(
1741
+ "na<double>()", vname, ptype
1742
+ )
1743
+ lines.append(
1744
+ f" {cpp_type} {cloned_safe} = {pending};"
1745
+ )
954
1746
  else:
955
1747
  lines.append(f" {cpp_type} {cloned_safe};")
956
1748
  return
@@ -961,6 +1753,52 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
961
1753
  else:
962
1754
  lines.append(f" double {cloned_safe} = 0.0;")
963
1755
 
1756
+ def _binding_is_series(
1757
+ self,
1758
+ raw_name: str,
1759
+ resolved_safe_name: str | None = None,
1760
+ ) -> bool:
1761
+ """Resolve Series-ness without conflating renamed persistent siblings.
1762
+
1763
+ ``ctx.series_vars`` is a legacy raw-name union. When a persistent
1764
+ member identity is known, its exact membership wins; otherwise keep the
1765
+ established raw-name behavior for ordinary non-var series, bar
1766
+ builtins, and parameters. A call-site clone resolves to ``h_cs1`` but
1767
+ inherits the base member ``h`` status, while a sibling rename such as
1768
+ ``x__blk1`` is itself an exact member and therefore wins directly.
1769
+ """
1770
+ base_safe = self._safe_name(raw_name)
1771
+ resolved = resolved_safe_name or base_safe
1772
+ if raw_name in self._lexical_series_bindings:
1773
+ return self._lexical_series_bindings[raw_name]
1774
+ if resolved in self._persistent_var_member_names:
1775
+ return resolved in self._series_var_member_names
1776
+ if (raw_name in self._qualified_func_var_raw_names
1777
+ and raw_name in self._ordinary_global_binding_names):
1778
+ return raw_name in self._ordinary_global_series_names
1779
+ if base_safe in self._persistent_var_member_names:
1780
+ return base_safe in self._series_var_member_names
1781
+ return raw_name in self.ctx.series_vars
1782
+
1783
+ def _known_var_is_lexically_shadowed(self, name: str) -> bool:
1784
+ """Whether a local/parameter/loop binder hides a known global name."""
1785
+ return name in self._lexical_known_var_tombstones
1786
+
1787
+ def _decl_binding_is_series(self, node_id: int, raw_name: str) -> bool:
1788
+ """Return exact history status for one declaration binding.
1789
+
1790
+ A VarDecl has one name, but a TupleAssign node owns several names. The
1791
+ node-only legacy set therefore cannot distinguish ``[x, y]`` when only
1792
+ one element is history-referenced. Prefer the exact analyzer key and
1793
+ retain the node-only fallback for older/manually-built contexts.
1794
+ """
1795
+ exact = getattr(self.ctx, "series_decl_bindings", set()) or set()
1796
+ if (node_id, raw_name) in exact:
1797
+ return True
1798
+ if any(binding_node == node_id for binding_node, _ in exact):
1799
+ return False
1800
+ return node_id in getattr(self.ctx, "series_decl_nodes", set())
1801
+
964
1802
  @staticmethod
965
1803
  def _int_literal_value(node: ASTNode | None) -> int | None:
966
1804
  """Return the integer value of a (possibly unary-minus) NumberLiteral,
@@ -1018,6 +1856,29 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1018
1856
  ``global_expr_map``; without registering it here, ``m`` was emitted as a scalar
1019
1857
  while ``on_bar`` still assigned ``PineMatrix``.
1020
1858
  """
1859
+ # A typed ``matrix<T> m = na`` or an inferred nullable selection has no
1860
+ # constructor call for the legacy RHS scan below to recognize. Resolve
1861
+ # declarations in source order from the authored hint/RHS, not from
1862
+ # ``ctx.collection_types``: that raw-name table describes the analyzer's
1863
+ # final state and can already contain an invalid later reassignment's
1864
+ # element type. The original declaration must remain the compatibility
1865
+ # baseline used to reject matrix<int> := matrix<float>.
1866
+ for stmt, _in_loop in self._walk_global_scope_with_loopflag(
1867
+ getattr(self.ctx.ast, "body", []), False
1868
+ ):
1869
+ if not isinstance(stmt, VarDecl):
1870
+ continue
1871
+ if stmt.type_hint:
1872
+ declared_spec = self._type_spec_from_hint_name(stmt.type_hint)
1873
+ elif isinstance(stmt.value, (Ternary, IfStmt, SwitchStmt)):
1874
+ declared_spec = self._type_spec_from_expr(stmt.value)
1875
+ else:
1876
+ continue
1877
+ if declared_spec is None or declared_spec.kind != "matrix":
1878
+ continue
1879
+ self._matrix_specs.setdefault(stmt.name, declared_spec)
1880
+ self._collection_types[stmt.name] = self._matrix_specs[stmt.name]
1881
+
1021
1882
  gem = getattr(self.ctx, "global_expr_map", {}) or {}
1022
1883
  for name, _ptype in self.ctx.global_var_decls:
1023
1884
  expr = gem.get(name)
@@ -1099,7 +1960,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1099
1960
  a separate scope and must not be attributed to a global member. The
1100
1961
  ``in_loop`` flag is True once inside any for/while loop body."""
1101
1962
  for s in stmts:
1102
- if isinstance(s, FuncDef):
1963
+ if isinstance(s, (FuncDef, MethodDef)):
1103
1964
  continue
1104
1965
  yield s, in_loop
1105
1966
  child_in_loop = in_loop or isinstance(s, (ForStmt, ForInStmt, WhileStmt))
@@ -1113,47 +1974,6 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1113
1974
  if isinstance(case_stmts, list):
1114
1975
  yield from self._walk_global_scope_with_loopflag(case_stmts, child_in_loop)
1115
1976
 
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
-
1157
1977
  def _extract_receiver_name(self, call_node) -> str | None:
1158
1978
  """Extract receiver Identifier name from m.method(...) or matrix.method(m, ...).
1159
1979
 
@@ -1216,8 +2036,70 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1216
2036
  else:
1217
2037
  self._codegen_error(node, "matrix.sort requires int, bool, string, or float element type; UDT matrices cannot be sorted")
1218
2038
 
2039
+ def _iter_declared_matrix_specs(self):
2040
+ """Yield matrix TypeSpecs reachable from declared source types.
2041
+
2042
+ A typed ``matrix<T> x = na`` has no matrix call for a syntactic scan
2043
+ to find. Matrix types can also be nested in UDT fields or appear only
2044
+ at a callable boundary, so include selection must start from analyzer
2045
+ metadata and recurse through aggregate types.
2046
+ """
2047
+ roots = list(self._collection_types.values())
2048
+ roots.extend(
2049
+ spec
2050
+ for specs in self._func_collection_types.values()
2051
+ for spec in specs.values()
2052
+ )
2053
+ roots.extend(
2054
+ spec
2055
+ for specs in self._block_collection_types.values()
2056
+ for spec in specs.values()
2057
+ if spec is not None
2058
+ )
2059
+ roots.extend(
2060
+ spec
2061
+ for fields in self._udt_field_type_specs.values()
2062
+ for spec in fields.values()
2063
+ )
2064
+ for fi in self.ctx.func_infos:
2065
+ roots.extend(
2066
+ spec
2067
+ for spec in (getattr(fi, "param_type_specs", []) or [])
2068
+ if spec is not None
2069
+ )
2070
+ return_spec = getattr(fi, "return_type_spec", None)
2071
+ if return_spec is not None:
2072
+ roots.append(return_spec)
2073
+
2074
+ def walk(spec: TypeSpec | None, visiting_udts: frozenset[str]):
2075
+ if spec is None:
2076
+ return
2077
+ if spec.kind == "matrix":
2078
+ yield spec
2079
+ return
2080
+ if spec.kind == "array":
2081
+ yield from walk(spec.element, visiting_udts)
2082
+ return
2083
+ if spec.kind == "map":
2084
+ yield from walk(spec.key, visiting_udts)
2085
+ yield from walk(spec.value, visiting_udts)
2086
+ return
2087
+ if spec.kind == "udt" and spec.name:
2088
+ if spec.name in visiting_udts:
2089
+ return
2090
+ nested_visiting = visiting_udts | {spec.name}
2091
+ for field_spec in self._udt_field_type_specs.get(
2092
+ spec.name, {}
2093
+ ).values():
2094
+ yield from walk(field_spec, nested_visiting)
2095
+
2096
+ for root in roots:
2097
+ yield from walk(root, frozenset())
2098
+
1219
2099
  def _detect_matrix_usage(self) -> bool:
1220
- """True if emitted C++ will need runtime/matrix.hpp (PineMatrix)."""
2100
+ """True if emitted C++ will need a matrix runtime header."""
2101
+ if next(self._iter_declared_matrix_specs(), None) is not None:
2102
+ return True
1221
2103
  for _, _, init_str in self.ctx.var_members:
1222
2104
  if init_str and "matrix.new" in str(init_str):
1223
2105
  return True
@@ -1228,6 +2110,90 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1228
2110
  return True
1229
2111
  return False
1230
2112
 
2113
+ def _detect_generic_matrix_usage(self) -> bool:
2114
+ """True if emitted C++ mentions a non-float matrix specialization."""
2115
+ float_spec = TypeSpec.primitive("float")
2116
+ if any(
2117
+ spec.element != float_spec
2118
+ for spec in self._iter_declared_matrix_specs()
2119
+ ):
2120
+ return True
2121
+
2122
+ # A standalone matrix.new<T>() expression may have no declaration
2123
+ # TypeSpec. Read its explicit template argument directly so the
2124
+ # generated call still receives the generic runtime declaration.
2125
+ for node in self._walk_ast(self.ctx.ast):
2126
+ if not isinstance(node, FuncCall):
2127
+ continue
2128
+ fn, ns = self._resolve_callee(node.callee)
2129
+ if ns != "matrix" or fn != "new":
2130
+ continue
2131
+ targs = self._template_args_from_call(node)
2132
+ if not targs:
2133
+ continue
2134
+ elem_spec = self._type_spec_from_hint_name(targs[0])
2135
+ if elem_spec is not None and elem_spec != float_spec:
2136
+ return True
2137
+ return False
2138
+
2139
+ def _detect_map_usage(self) -> bool:
2140
+ """True when emitted C++ needs the PineMap handle/runtime helpers.
2141
+
2142
+ The call scan catches constructors and built-in operations. TypeSpec
2143
+ scans additionally catch typed ``na`` maps and map fields that never
2144
+ execute a map call, so their declarations still receive map.hpp and
2145
+ map-aware COOF checkpoint support.
2146
+ """
2147
+ def contains_map(
2148
+ spec: TypeSpec | None, visiting_udts: frozenset[str] = frozenset()
2149
+ ) -> bool:
2150
+ if spec is None:
2151
+ return False
2152
+ if spec.kind == "map":
2153
+ return True
2154
+ if spec.kind in {"array", "matrix"}:
2155
+ return contains_map(spec.element, visiting_udts)
2156
+ if spec.kind == "udt" and spec.name:
2157
+ if spec.name in visiting_udts:
2158
+ return False
2159
+ nested_visiting = visiting_udts | {spec.name}
2160
+ return any(
2161
+ contains_map(field_spec, nested_visiting)
2162
+ for field_spec in self._udt_field_type_specs.get(
2163
+ spec.name, {}
2164
+ ).values()
2165
+ )
2166
+ return False
2167
+
2168
+ if self._map_vars:
2169
+ return True
2170
+ if any(contains_map(spec) for spec in self._collection_types.values()):
2171
+ return True
2172
+ if any(
2173
+ contains_map(spec)
2174
+ for fields in self._udt_field_type_specs.values()
2175
+ for spec in fields.values()
2176
+ ):
2177
+ return True
2178
+ for specs in self._func_collection_types.values():
2179
+ if any(contains_map(spec) for spec in specs.values()):
2180
+ return True
2181
+ for fi in self._func_info_map.values():
2182
+ if any(contains_map(spec) for spec in fi.param_type_specs):
2183
+ return True
2184
+ if contains_map(getattr(fi, "return_type_spec", None)):
2185
+ return True
2186
+ for node in self._walk_ast(self.ctx.ast):
2187
+ if isinstance(node, FuncCall):
2188
+ _fn, namespace = self._resolve_callee(node.callee)
2189
+ if namespace == "map":
2190
+ return True
2191
+ if isinstance(node, VarDecl):
2192
+ spec = self._type_spec_from_hint_name(node.type_hint)
2193
+ if contains_map(spec):
2194
+ return True
2195
+ return False
2196
+
1231
2197
  # The type-inference helpers (_type_spec_*, _infer_type, _array_method_expr,
1232
2198
  # _map_method_expr, _template_args_from_call, ...) live on TypeInferer
1233
2199
  # — see codegen/types.py.
@@ -1298,14 +2264,17 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1298
2264
  ``_known_vars`` (no use-site inlining): only the length-analysis path is
1299
2265
  affected, and the ``var`` member still emits and initializes normally.
1300
2266
  """
2267
+ literal_candidates = self._admitted_stable_var_ctor_literals(reassigned)
2268
+
1301
2269
  for stmt in (self.ctx.ast.body or []):
1302
2270
  if not isinstance(stmt, VarDecl):
1303
2271
  continue
1304
2272
  if not (stmt.is_var or stmt.is_varip):
1305
2273
  continue
2274
+ literal_record = literal_candidates.get(stmt.name)
1306
2275
  if stmt.name in reassigned:
1307
2276
  continue
1308
- if stmt.name in self.ctx.series_vars:
2277
+ if self._decl_binding_is_series(id(stmt), stmt.name):
1309
2278
  continue
1310
2279
  if stmt.value is None or not self._expr_is_stable(stmt.value):
1311
2280
  continue
@@ -1314,6 +2283,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1314
2283
  continue
1315
2284
  self._derived_input_expr[stmt.name] = expr_str
1316
2285
  self._stable_runtime_vars.add(stmt.name)
2286
+ if literal_record is not None:
2287
+ self._stable_var_ctor_literals[stmt.name] = literal_record
1317
2288
  # Mark input-backed iff the init references an input, so the reset
1318
2289
  # emits override-aware get_input_*() reads for it.
1319
2290
  import re as _re
@@ -1321,6 +2292,215 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1321
2292
  if any(t in self._input_backed_vars for t in toks):
1322
2293
  self._input_backed_vars.add(stmt.name)
1323
2294
 
2295
+ @staticmethod
2296
+ def _is_direct_int_var_literal(node) -> bool:
2297
+ """Whether ``node`` is the one literal persistent shape we admit."""
2298
+
2299
+ return (
2300
+ isinstance(node, VarDecl)
2301
+ and node.is_var
2302
+ and not node.is_varip
2303
+ and node.type_hint in (None, "int")
2304
+ and isinstance(node.value, NumberLiteral)
2305
+ and isinstance(node.value.value, int)
2306
+ and not isinstance(node.value.value, bool)
2307
+ )
2308
+
2309
+ @staticmethod
2310
+ def _walk_candidate_surface(root):
2311
+ """Yield every AST node reachable through compiler-owned metadata.
2312
+
2313
+ The regular codegen walker intentionally follows only historically
2314
+ emitted statement/expression slots. Literal admission is a safety
2315
+ proof and therefore needs the larger authored surface: loop bounds and
2316
+ iterables, enum/type defaults, function/method parameter defaults kept
2317
+ in ``annotations``, and any future AST-bearing metadata. ``vars()``
2318
+ plus identity de-duplication is both exhaustive and cycle-safe.
2319
+ """
2320
+
2321
+ seen: set[int] = set()
2322
+
2323
+ def walk(value):
2324
+ if value is None or isinstance(
2325
+ value, (str, bytes, int, float, bool)
2326
+ ):
2327
+ return
2328
+ value_id = id(value)
2329
+ if value_id in seen:
2330
+ return
2331
+ seen.add(value_id)
2332
+
2333
+ if isinstance(value, ASTNode):
2334
+ yield value
2335
+ if isinstance(value, dict):
2336
+ for key, child in value.items():
2337
+ yield from walk(key)
2338
+ yield from walk(child)
2339
+ return
2340
+ if isinstance(value, (list, tuple, set, frozenset)):
2341
+ for child in value:
2342
+ yield from walk(child)
2343
+ return
2344
+ if hasattr(value, "__dict__"):
2345
+ for child in vars(value).values():
2346
+ yield from walk(child)
2347
+
2348
+ yield from walk(root)
2349
+
2350
+ def _stable_var_literal_has_earlier_use(
2351
+ self,
2352
+ name: str,
2353
+ declaration_index: int,
2354
+ ) -> bool:
2355
+ """Whether authored code reads ``name`` before its global declaration."""
2356
+
2357
+ for root in (self.ctx.ast.body or [])[:declaration_index]:
2358
+ if any(
2359
+ isinstance(node, Identifier) and node.name == name
2360
+ for node in self._walk_candidate_surface(root)
2361
+ ):
2362
+ return True
2363
+ return False
2364
+
2365
+ def _candidate_surface_mentions_name(self, root, name: str) -> bool:
2366
+ return any(
2367
+ isinstance(node, Identifier) and node.name == name
2368
+ for node in self._walk_candidate_surface(root)
2369
+ )
2370
+
2371
+ def _stable_var_literal_has_unsupported_declaration(self) -> bool:
2372
+ """Whether authored metadata would require cross-boundary dataflow.
2373
+
2374
+ Stable-var constructor folding is deliberately declaration-local. A
2375
+ user function, method, type, or enum introduces defaults, aliases, or
2376
+ receiver effects that require a wider proof, so this narrow feature
2377
+ fails closed for the whole script instead of approximating that proof.
2378
+ """
2379
+
2380
+ return any(
2381
+ isinstance(node, (FuncDef, MethodDef, TypeDecl, EnumDecl))
2382
+ for node in self._walk_candidate_surface(self.ctx.ast)
2383
+ )
2384
+
2385
+ def _stable_var_literal_has_parse_recovery(self) -> bool:
2386
+ """Whether the parser discarded any authored source fragment.
2387
+
2388
+ Recovery intentionally keeps broad corpus transpilation moving, but a
2389
+ discarded declaration can hide aliases or override a builtin method.
2390
+ Literal admission therefore requires a lossless parse even though all
2391
+ existing non-admission codegen paths retain their recovery behavior.
2392
+ """
2393
+
2394
+ return bool(
2395
+ (self.ctx.ast.annotations or {}).get("parse_recovery_count", 0)
2396
+ )
2397
+
2398
+ def _stable_var_literal_has_unsupported_receiver_use(
2399
+ self,
2400
+ name: str,
2401
+ ) -> bool:
2402
+ """Catch primitive extension-method syntax dropped by the parser."""
2403
+
2404
+ roots: list[object] = [self.ctx.ast]
2405
+ roots.extend(
2406
+ pragma.expr_node
2407
+ for pragma in (getattr(self.ctx, "pf_trace_pragmas", None) or [])
2408
+ if getattr(pragma, "expr_node", None) is not None
2409
+ )
2410
+ for root in roots:
2411
+ for node in self._walk_candidate_surface(root):
2412
+ if (
2413
+ isinstance(node, FuncCall)
2414
+ and isinstance(node.callee, MemberAccess)
2415
+ and self._candidate_surface_mentions_name(
2416
+ node.callee.object, name
2417
+ )
2418
+ ):
2419
+ return True
2420
+ return False
2421
+
2422
+ def _stable_var_literal_has_history_use(self, name: str) -> bool:
2423
+ """Whether any authored or post-analyzer expression histories ``name``."""
2424
+
2425
+ roots: list[object] = [self.ctx.ast]
2426
+ roots.extend(
2427
+ pragma.expr_node
2428
+ for pragma in (getattr(self.ctx, "pf_trace_pragmas", None) or [])
2429
+ if getattr(pragma, "expr_node", None) is not None
2430
+ )
2431
+ for root in roots:
2432
+ for node in self._walk_candidate_surface(root):
2433
+ if not isinstance(node, Subscript):
2434
+ continue
2435
+ if any(
2436
+ isinstance(receiver_node, Identifier)
2437
+ and receiver_node.name == name
2438
+ for receiver_node in self._walk_candidate_surface(
2439
+ node.object
2440
+ )
2441
+ ):
2442
+ return True
2443
+ return False
2444
+
2445
+ def _admitted_stable_var_ctor_literals(
2446
+ self,
2447
+ reassigned: set[str],
2448
+ ) -> dict[str, _StableVarCtorLiteral]:
2449
+ """Return declaration-exact persistent literals safe for TA sizing."""
2450
+
2451
+ occurrences: dict[str, list[tuple[int, VarDecl]]] = {}
2452
+ for index, stmt in enumerate(self.ctx.ast.body or []):
2453
+ if self._is_direct_int_var_literal(stmt):
2454
+ occurrences.setdefault(stmt.name, []).append((index, stmt))
2455
+
2456
+ unique = {
2457
+ name: items[0]
2458
+ for name, items in occurrences.items()
2459
+ if len(items) == 1
2460
+ }
2461
+ if not unique:
2462
+ return {}
2463
+
2464
+ candidate_names = set(unique)
2465
+ binding_counts = {name: 0 for name in candidate_names}
2466
+ for node in self._walk_candidate_surface(self.ctx.ast):
2467
+ names: list[str] = []
2468
+ if isinstance(node, VarDecl) and node.name:
2469
+ names.append(node.name)
2470
+ elif isinstance(node, TupleAssign):
2471
+ names.extend(name for name in node.names if name != "_")
2472
+ elif isinstance(node, ForStmt) and node.var:
2473
+ names.append(node.var)
2474
+ elif isinstance(node, ForInStmt):
2475
+ if node.var:
2476
+ names.append(node.var)
2477
+ names.extend(name for name in (node.vars or []) if name != "_")
2478
+ if isinstance(node, (FuncDef, MethodDef)):
2479
+ names.extend(name for name in node.params if name != "_")
2480
+ for bound_name in names:
2481
+ if bound_name in binding_counts:
2482
+ binding_counts[bound_name] += 1
2483
+
2484
+ admitted: dict[str, _StableVarCtorLiteral] = {}
2485
+ for name, (index, declaration) in unique.items():
2486
+ if (
2487
+ name in reassigned
2488
+ or binding_counts.get(name) != 1
2489
+ or self._decl_binding_is_series(id(declaration), name)
2490
+ or self._stable_var_literal_has_earlier_use(name, index)
2491
+ or self._stable_var_literal_has_history_use(name)
2492
+ or self._stable_var_literal_has_unsupported_declaration()
2493
+ or self._stable_var_literal_has_parse_recovery()
2494
+ or self._stable_var_literal_has_unsupported_receiver_use(name)
2495
+ ):
2496
+ continue
2497
+ admitted[name] = _StableVarCtorLiteral(
2498
+ value=declaration.value.value,
2499
+ declaration_id=id(declaration),
2500
+ top_level_index=index,
2501
+ )
2502
+ return admitted
2503
+
1324
2504
  def _collect_reassigned_stable_scalars(self, reassigned: set[str]) -> None:
1325
2505
  """Track class-scope scalars that are reassigned but only along stable
1326
2506
  if/elif paths (see ``test_stable_reassigned_class_scope_length``).
@@ -1619,7 +2799,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1619
2799
  return True
1620
2800
  if isinstance(node, Identifier):
1621
2801
  name = node.name
1622
- if name in self._known_vars:
2802
+ if self._known_var_is_lexically_shadowed(name):
2803
+ return False
2804
+ if (name in self._known_vars
2805
+ and not self._known_var_is_lexically_shadowed(name)):
1623
2806
  return True
1624
2807
  if name in self._stable_runtime_vars:
1625
2808
  return True
@@ -1794,7 +2977,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1794
2977
  def _collect_known_var(self, node: VarDecl) -> None:
1795
2978
  """Extract known constant value from a VarDecl."""
1796
2979
  # Don't inline series variables — their values change over time
1797
- if node.name in self.ctx.series_vars:
2980
+ if self._decl_binding_is_series(id(node), node.name):
1798
2981
  return
1799
2982
  # Don't inline var/varip variables — they're mutable state that persists
1800
2983
  # across bars and can be reassigned with :=
@@ -1999,7 +3182,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1999
3182
  """
2000
3183
  self._inline_history_members = []
2001
3184
  self._inline_history_member_by_key = {}
2002
- counters = {"hist_call": 0, "series_arg": 0}
3185
+ counters = {
3186
+ "hist_call": 0,
3187
+ "series_arg": 0,
3188
+ "udf_series_arg": 0,
3189
+ }
2003
3190
 
2004
3191
  def walk_nodes(value):
2005
3192
  """Yield AST nodes in stable field order, including tuple elements.
@@ -2032,29 +3219,252 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2032
3219
  for child in walk_nodes(fi.node):
2033
3220
  owner_by_node[id(child)] = fi.name
2034
3221
 
3222
+ # ``ctx.series_vars`` is a legacy raw-name union. A raw name can bind
3223
+ # both an exact Series declaration and an unrelated scalar declaration
3224
+ # (sibling blocks, or a callable local shadowing a global). Such calls
3225
+ # may need a synthetic history bridge even though the raw name appears
3226
+ # in that union, so reserve the bridge up front; source-order lexical
3227
+ # state decides whether the emitted call actually uses it.
3228
+ binding_series_states: dict[str, set[bool]] = {}
3229
+ var_metadata = getattr(
3230
+ self.ctx, "var_member_metadata_by_node", {}
3231
+ ) or {}
3232
+ for decl in walk_nodes(self.ctx.ast):
3233
+ if isinstance(decl, VarDecl):
3234
+ metadata = var_metadata.get(id(decl))
3235
+ if metadata is not None:
3236
+ exact_member = self._safe_name(metadata[1])
3237
+ is_series = exact_member in self._series_var_member_names
3238
+ else:
3239
+ is_series = self._decl_binding_is_series(
3240
+ id(decl), decl.name
3241
+ )
3242
+ binding_series_states.setdefault(decl.name, set()).add(
3243
+ is_series
3244
+ )
3245
+ elif isinstance(decl, TupleAssign):
3246
+ for name in decl.names:
3247
+ if name != "_":
3248
+ binding_series_states.setdefault(name, set()).add(
3249
+ self._decl_binding_is_series(id(decl), name)
3250
+ )
3251
+ elif isinstance(decl, ForStmt):
3252
+ if decl.var:
3253
+ binding_series_states.setdefault(decl.var, set()).add(
3254
+ False
3255
+ )
3256
+ elif isinstance(decl, ForInStmt):
3257
+ loop_names = (
3258
+ [decl.var]
3259
+ if decl.var
3260
+ else list(decl.vars or [])
3261
+ )
3262
+ for name in loop_names:
3263
+ if name and name != "_":
3264
+ binding_series_states.setdefault(name, set()).add(
3265
+ False
3266
+ )
3267
+ ambiguous_series_names = {
3268
+ name for name, states in binding_series_states.items()
3269
+ if len(states) > 1
3270
+ }
3271
+
3272
+ def register_one(
3273
+ kind: str,
3274
+ source_key: tuple,
3275
+ cpp_type: str,
3276
+ context: str | None,
3277
+ ) -> None:
3278
+ if cpp_type not in ("double", "int", "int64_t", "bool"):
3279
+ cpp_type = "double"
3280
+ key = (kind, *source_key, context)
3281
+ if key in self._inline_history_member_by_key:
3282
+ return
3283
+ counters[kind] += 1
3284
+ member_name = f"_{kind}_{counters[kind]}"
3285
+ self._inline_history_member_by_key[key] = member_name
3286
+ self._inline_history_members.append({
3287
+ "kind": kind,
3288
+ "member_name": member_name,
3289
+ "cpp_type": cpp_type,
3290
+ "context": context,
3291
+ })
3292
+
2035
3293
  def register(kind: str, source_key: tuple, cpp_type: str,
2036
3294
  owner: str | None) -> None:
2037
- if cpp_type not in ("double", "int", "bool"):
2038
- cpp_type = "double"
2039
3295
  for context in self._inline_history_contexts_for_owner(owner):
2040
- key = (kind, *source_key, context)
2041
- if key in self._inline_history_member_by_key:
2042
- continue
2043
- counters[kind] += 1
2044
- member_name = f"_{kind}_{counters[kind]}"
2045
- self._inline_history_member_by_key[key] = member_name
2046
- self._inline_history_members.append({
2047
- "kind": kind,
2048
- "member_name": member_name,
2049
- "cpp_type": cpp_type,
2050
- "context": context,
2051
- })
3296
+ register_one(kind, source_key, cpp_type, context)
2052
3297
 
2053
- def actual_args_for(call: FuncCall, params: list[str]) -> list:
3298
+ def owner_cs_for_context(
3299
+ owner: str | None, context: str | None
3300
+ ) -> int | None:
3301
+ if owner is None or context is None:
3302
+ return None
3303
+ prefix = f"{self._func_cpp_base_name(owner)}_cs"
3304
+ if not context.startswith(prefix):
3305
+ return None
3306
+ suffix = context[len(prefix):]
3307
+ return int(suffix) if suffix.isdigit() else None
3308
+
3309
+ def target_cs_for_context(
3310
+ fi,
3311
+ call: FuncCall,
3312
+ owner: str | None,
3313
+ context: str | None,
3314
+ ) -> int | None:
3315
+ owner_cs = owner_cs_for_context(owner, context)
3316
+ cs_info = self.ctx.func_call_cs_map.get(id(call))
3317
+ count = self.ctx.func_call_site_counts.get(fi.name, 0)
3318
+ if owner_cs is not None and (
3319
+ cs_info is not None or count > 1
3320
+ ):
3321
+ return owner_cs
3322
+ if cs_info is not None and cs_info[0] == fi.name:
3323
+ return cs_info[1]
3324
+ return None
3325
+
3326
+ def actual_args_for(call: FuncCall, params: list[str], method_info=None) -> list:
3327
+ if method_info is not None:
3328
+ return list(
3329
+ self._bind_typed_method_args(method_info, call).args_by_param
3330
+ )
2054
3331
  if call.kwargs:
2055
3332
  return _merge_kwargs(call.args, call.kwargs, params, lambda arg: arg)
2056
3333
  return list(call.args)
2057
3334
 
3335
+ def owner_lexical_specs(owner: str | None) -> dict[str, TypeSpec | None]:
3336
+ """Recover callable parameter types after analyzer scopes exit."""
3337
+ if owner is None:
3338
+ return {}
3339
+ info = self._func_info_map.get(owner)
3340
+ if info is None or info.node is None:
3341
+ return {}
3342
+ specs = list(getattr(info, "param_type_specs", ()) or ())
3343
+ lexical = {
3344
+ name: specs[index] if index < len(specs) else None
3345
+ for index, name in enumerate(info.node.params)
3346
+ }
3347
+ if (
3348
+ getattr(info, "is_udt_method", False)
3349
+ and info.node.params
3350
+ and info.udt_type_name
3351
+ ):
3352
+ receiver_spec = specs[0] if specs else None
3353
+ if receiver_spec is None:
3354
+ receiver_spec = self._type_spec_from_hint_name(
3355
+ info.udt_type_name
3356
+ )
3357
+ lexical[info.node.params[0]] = receiver_spec
3358
+ lexical.update(self._func_collection_types.get(owner, {}))
3359
+ return lexical
3360
+
3361
+ def plain_udf_info(call: FuncCall):
3362
+ if not isinstance(call.callee, Identifier):
3363
+ return None
3364
+ candidate = self._func_info_map.get(call.callee.name)
3365
+ if (
3366
+ candidate is None
3367
+ or candidate.node is None
3368
+ or getattr(candidate, "is_udt_method", False)
3369
+ ):
3370
+ return None
3371
+ return candidate
3372
+
3373
+ # Requested-context UDF expressions are lowered independently by
3374
+ # security.py. Classify their otherwise-emitted helper variants so
3375
+ # this chart-clocked factor neither allocates unused buffers for them
3376
+ # nor changes the legacy bodies that remain in generated C++.
3377
+ requested_node_ids: set[int] = set()
3378
+ for security_call in self._security_calls:
3379
+ expression = security_call.get("expr_node")
3380
+ if expression is None:
3381
+ continue
3382
+ requested_node_ids.update(
3383
+ id(child) for child in walk_nodes(expression)
3384
+ )
3385
+ self._requested_context_inline_node_ids = requested_node_ids
3386
+
3387
+ def emitted_context_for_call(
3388
+ fi,
3389
+ call: FuncCall,
3390
+ owner: str | None,
3391
+ owner_context: str | None,
3392
+ ) -> str | None:
3393
+ dispatch = self._instance_dispatch.get(
3394
+ (owner_context, id(call))
3395
+ )
3396
+ if dispatch is not None:
3397
+ return dispatch
3398
+ target_cs = target_cs_for_context(
3399
+ fi, call, owner, owner_context
3400
+ )
3401
+ if target_cs is not None:
3402
+ return f"{self._func_cpp_base_name(fi.name)}_cs{target_cs}"
3403
+ return None
3404
+
3405
+ requested_roots: set[tuple[str, str | None]] = set()
3406
+ chart_roots: set[tuple[str, str | None]] = set()
3407
+ context_edges: dict[
3408
+ tuple[str, str | None], set[tuple[str, str | None]]
3409
+ ] = {}
3410
+ for call in (
3411
+ candidate
3412
+ for candidate in walk_nodes(self.ctx.ast)
3413
+ if isinstance(candidate, FuncCall)
3414
+ ):
3415
+ fi = plain_udf_info(call)
3416
+ if fi is None:
3417
+ continue
3418
+ owner = owner_by_node.get(id(call))
3419
+ if id(call) in requested_node_ids:
3420
+ owner_contexts = (
3421
+ [None]
3422
+ if owner is None
3423
+ else self._inline_history_contexts_for_owner(owner)
3424
+ )
3425
+ for owner_context in owner_contexts:
3426
+ requested_roots.add((
3427
+ fi.name,
3428
+ emitted_context_for_call(
3429
+ fi, call, owner, owner_context
3430
+ ),
3431
+ ))
3432
+ continue
3433
+ if owner is None:
3434
+ target = (
3435
+ fi.name,
3436
+ emitted_context_for_call(fi, call, None, None),
3437
+ )
3438
+ chart_roots.add(target)
3439
+ continue
3440
+ for owner_context in self._inline_history_contexts_for_owner(owner):
3441
+ source = (owner, owner_context)
3442
+ target = (
3443
+ fi.name,
3444
+ emitted_context_for_call(
3445
+ fi, call, owner, owner_context
3446
+ ),
3447
+ )
3448
+ context_edges.setdefault(source, set()).add(target)
3449
+
3450
+ def reachable(
3451
+ roots: set[tuple[str, str | None]],
3452
+ ) -> set[tuple[str, str | None]]:
3453
+ found = set(roots)
3454
+ pending = list(roots)
3455
+ while pending:
3456
+ source = pending.pop()
3457
+ for target in context_edges.get(source, ()):
3458
+ if target in found:
3459
+ continue
3460
+ found.add(target)
3461
+ pending.append(target)
3462
+ return found
3463
+
3464
+ self._requested_context_only_inline_contexts = (
3465
+ reachable(requested_roots) - reachable(chart_roots)
3466
+ )
3467
+
2058
3468
  for node in walk_nodes(self.ctx.ast):
2059
3469
  owner = owner_by_node.get(id(node))
2060
3470
  if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
@@ -2064,8 +3474,28 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2064
3474
 
2065
3475
  if not isinstance(node, FuncCall):
2066
3476
  continue
2067
- func_name, _ = self._resolve_callee(node.callee)
2068
- fi = self._func_info_map.get(func_name)
3477
+ fi = None
3478
+ method_call = False
3479
+ if isinstance(node.callee, MemberAccess):
3480
+ # Method syntax is type-directed. Resolve it before considering
3481
+ # any same-named bare UDF, and use the AST owner's lexical
3482
+ # parameter types: analyzer symbol scopes no longer exist in
3483
+ # this source-order prepass.
3484
+ receiver_spec = self._map_effect_type_spec(
3485
+ node.callee.object, owner_lexical_specs(owner)
3486
+ )
3487
+ receiver_name = method_receiver_type_name(receiver_spec)
3488
+ if receiver_name is not None:
3489
+ method_key = f"{receiver_name}.{node.callee.member}"
3490
+ candidate = self._func_info_map.get(method_key)
3491
+ if (
3492
+ candidate is not None
3493
+ and getattr(candidate, "is_udt_method", False)
3494
+ ):
3495
+ fi = candidate
3496
+ method_call = True
3497
+ elif isinstance(node.callee, Identifier):
3498
+ fi = self._func_info_map.get(node.callee.name)
2069
3499
  if fi is None or fi.node is None:
2070
3500
  continue
2071
3501
  func_sv = self.ctx.func_series_vars.get(fi.name, set())
@@ -2074,18 +3504,92 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2074
3504
  }
2075
3505
  if not series_param_indices:
2076
3506
  continue
2077
- args = actual_args_for(node, list(fi.node.params))
3507
+ if method_call:
3508
+ args = [
3509
+ node.callee.object,
3510
+ *actual_args_for(
3511
+ node, list(fi.node.params[1:]), method_info=fi
3512
+ ),
3513
+ ]
3514
+ else:
3515
+ args = actual_args_for(node, list(fi.node.params))
2078
3516
  for idx, arg in enumerate(args):
2079
3517
  if idx not in series_param_indices:
2080
3518
  continue
2081
- if isinstance(arg, Identifier):
2082
- if arg.name in BAR_FIELDS or arg.name in BAR_SERIES_PUSH:
2083
- continue
2084
- if arg.name in self.ctx.series_vars:
3519
+ for context in self._inline_history_contexts_for_owner(owner):
3520
+ target_cs = target_cs_for_context(
3521
+ fi, node, owner, context
3522
+ )
3523
+ expected_cpp_type = self._series_param_element_cpp_type(
3524
+ fi, idx, target_cs
3525
+ )
3526
+ # Pine gives each plain-UDF written call its own
3527
+ # chart-aligned parameter history. Even an exact Series
3528
+ # actual therefore needs a call-site-owned buffer: skipped
3529
+ # evaluations hold the last value in that bar's slot
3530
+ # instead of exposing the caller's independently changing
3531
+ # history. Typed methods retain their established direct
3532
+ # binding/bridge behavior below.
3533
+ chart_execution_context = (
3534
+ id(node) not in requested_node_ids
3535
+ and (owner, context)
3536
+ not in self._requested_context_only_inline_contexts
3537
+ )
3538
+ if not method_call and chart_execution_context:
3539
+ register_one(
3540
+ "udf_series_arg",
3541
+ (id(node), idx),
3542
+ expected_cpp_type,
3543
+ context,
3544
+ )
2085
3545
  continue
2086
- register(
2087
- "series_arg", (id(node), idx), self._infer_type(arg), owner
2088
- )
3546
+ needs_bridge = True
3547
+ if isinstance(arg, Identifier):
3548
+ if (
3549
+ (
3550
+ arg.name in BAR_FIELDS
3551
+ or arg.name in BAR_SERIES_PUSH
3552
+ )
3553
+ and expected_cpp_type == "double"
3554
+ ):
3555
+ needs_bridge = False
3556
+ owner_info = self._func_info_map.get(owner or "")
3557
+ owner_series_cpp_type = None
3558
+ if (
3559
+ owner_info is not None
3560
+ and owner_info.node is not None
3561
+ and arg.name in owner_info.node.params
3562
+ and arg.name in self.ctx.func_series_vars.get(
3563
+ owner_info.name, set()
3564
+ )
3565
+ ):
3566
+ owner_index = owner_info.node.params.index(
3567
+ arg.name
3568
+ )
3569
+ owner_series_cpp_type = (
3570
+ self._series_param_element_cpp_type(
3571
+ owner_info,
3572
+ owner_index,
3573
+ owner_cs_for_context(owner, context),
3574
+ )
3575
+ )
3576
+ if owner_series_cpp_type == expected_cpp_type:
3577
+ needs_bridge = False
3578
+ if (
3579
+ owner_series_cpp_type is None
3580
+ and arg.name in self.ctx.series_vars
3581
+ and arg.name not in ambiguous_series_names
3582
+ and self._series_type_for(arg.name)
3583
+ == expected_cpp_type
3584
+ ):
3585
+ needs_bridge = False
3586
+ if needs_bridge:
3587
+ register_one(
3588
+ "series_arg",
3589
+ (id(node), idx),
3590
+ expected_cpp_type,
3591
+ context,
3592
+ )
2089
3593
 
2090
3594
  def _inline_history_member(self, kind: str, node: ASTNode,
2091
3595
  arg_idx: int | None = None) -> str:
@@ -2100,12 +3604,153 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2100
3604
  )
2101
3605
  return member
2102
3606
 
3607
+ @staticmethod
3608
+ def _allocate_generated_cpp_name(preferred: str, used: set[str]) -> str:
3609
+ """Reserve one deterministic generated C++ identifier.
3610
+
3611
+ Pine permits identifiers beginning with underscores, so generated
3612
+ support names cannot rely on a magic prefix being unavailable to the
3613
+ author. Preserve the historical spelling when it is free and append
3614
+ a stable suffix only for an actual collision.
3615
+ """
3616
+ candidate = preferred
3617
+ suffix = 2
3618
+ while candidate in used:
3619
+ candidate = f"{preferred}__pf{suffix}"
3620
+ suffix += 1
3621
+ used.add(candidate)
3622
+ return candidate
3623
+
3624
+ def _prepare_udt_generated_names(self) -> None:
3625
+ """Allocate UDT support symbols outside every authored namespace.
3626
+
3627
+ Record/trait/template names live at generated-file scope, while arena
3628
+ instances live in ``GeneratedStrategy``. This pass runs only after
3629
+ callable clones, declaration-site flags and inline-history members have
3630
+ their final names. Keep one complete class-scope inventory so adding a
3631
+ UDT member cannot make ``_func_safe_name`` change after those prepasses,
3632
+ then extend the file-scope inventory with the same names because an
3633
+ unqualified class member, parameter or local can hide a generated
3634
+ support type. In the ordinary non-colliding case the preferred names
3635
+ remain byte-for-byte unchanged.
3636
+ """
3637
+ class_used: set[str] = set(
3638
+ getattr(self, "_runtime_var_init_flag_used_names", ())
3639
+ )
3640
+ class_used.update(self._all_member_names)
3641
+ class_used.update(
3642
+ self._safe_name(name) for name in self._all_bound_names
3643
+ )
3644
+ class_used.update(
3645
+ self._safe_name(name)
3646
+ for name, _ptype in self.ctx.global_var_decls
3647
+ )
3648
+ for func_info in self.ctx.func_infos:
3649
+ node = getattr(func_info, "node", None)
3650
+ if node is not None:
3651
+ class_used.update(
3652
+ self._safe_name(name)
3653
+ for name in (getattr(node, "params", ()) or ())
3654
+ )
3655
+
3656
+ # These generated members are not all recorded in
3657
+ # ``_runtime_var_init_flag_used_names``. Reserve them explicitly so
3658
+ # the inventory remains authoritative even for scripts without a
3659
+ # runtime scalar ``var`` initializer.
3660
+ class_used.update(
3661
+ remapped
3662
+ for remap in self._func_cs_ta_remap.values()
3663
+ for remapped in remap.values()
3664
+ )
3665
+ class_used.update(
3666
+ site.member_name
3667
+ for index, site in enumerate(self.ctx.ta_call_sites)
3668
+ if index not in self._dead_ta_indices
3669
+ )
3670
+ class_used.update(
3671
+ f"_precalc_{site.member_name}"
3672
+ for index, site in enumerate(self.ctx.ta_call_sites)
3673
+ if index not in self._dead_ta_indices
3674
+ and self._ta_site_uses_precalc(site)
3675
+ )
3676
+ class_used.update(
3677
+ site.member_name
3678
+ for index, site in enumerate(self.ctx.fixnan_sites)
3679
+ if index not in self._dead_fixnan_indices
3680
+ )
3681
+ class_used.update(
3682
+ info["member_name"] for info in self._inline_history_members
3683
+ )
3684
+ class_used.update(
3685
+ variant["member_name"]
3686
+ for info in self._security_eval_info
3687
+ for variants in (info.get("ta_variants") or {}).values()
3688
+ for variant in variants
3689
+ )
3690
+ for instance in self._fresh_instances:
3691
+ class_used.add(instance["name"])
3692
+ for remap_name in ("ta_remap", "var_remap", "fixnan_remap"):
3693
+ class_used.update(instance.get(remap_name, {}).values())
3694
+
3695
+ global_used: set[str] = set(self._udt_defs)
3696
+ global_used.update(self._enum_defs)
3697
+ global_used.update(
3698
+ f"{enum_name}_{member}"
3699
+ for enum_name, members in self._enum_defs.items()
3700
+ for member in members
3701
+ )
3702
+ global_used.update(
3703
+ f"{enum_name}_str_values"
3704
+ for enum_name, members in self._enum_defs.items()
3705
+ if self._enum_member_strings.get(enum_name)
3706
+ and len(self._enum_member_strings[enum_name]) == len(members)
3707
+ )
3708
+ global_used.update(class_used)
3709
+
3710
+ self._udt_arena_template_cpp_name = self._allocate_generated_cpp_name(
3711
+ "_PFUdtArena", global_used
3712
+ )
3713
+ self._udt_undo_coordinator_cpp_name = self._allocate_generated_cpp_name(
3714
+ "_PFUdtUndoCoordinator", global_used
3715
+ )
3716
+ self._checkpoint_traits_cpp_name = self._allocate_generated_cpp_name(
3717
+ "_PFCheckpointTraits", global_used
3718
+ )
3719
+ self._udt_record_cpp_names: dict[str, str] = {}
3720
+ for type_name in self._udt_defs:
3721
+ self._udt_record_cpp_names[type_name] = (
3722
+ self._allocate_generated_cpp_name(
3723
+ f"_PFUdtRecord_{type_name}", global_used
3724
+ )
3725
+ )
3726
+
3727
+ member_used = set(class_used)
3728
+ self._udt_undo_coordinator_member_name = self._allocate_generated_cpp_name(
3729
+ "_pf_udt_undo", member_used
3730
+ )
3731
+ self._all_member_names.add(self._udt_undo_coordinator_member_name)
3732
+ self._udt_arena_member_names: dict[str, str] = {}
3733
+ for type_name in self._udt_defs:
3734
+ preferred = f"_pf_udt_{self._safe_name(type_name)}"
3735
+ allocated = self._allocate_generated_cpp_name(
3736
+ preferred, member_used
3737
+ )
3738
+ self._udt_arena_member_names[type_name] = allocated
3739
+ self._all_member_names.add(allocated)
3740
+
2103
3741
  def generate(self) -> str:
2104
3742
  """Generate C++ source from the AnalyzerContext."""
2105
3743
  # Context-sensitive instance pre-pass (needs the naming helpers populated
2106
3744
  # in __init__). Computes nested stateful-helper dispatch + fresh instances.
2107
3745
  self._build_func_instances()
3746
+ # Allocate declaration-site once flags only after every natural and
3747
+ # context-sensitive callable-state member name is known.
3748
+ self._prepare_runtime_scalar_var_initializers()
2108
3749
  self._prepare_inline_history_members()
3750
+ # UDT support names are the final generated-name category. They must
3751
+ # suffix around every authored/callable identifier already frozen by
3752
+ # the passes above, never force an earlier callable to rename itself.
3753
+ self._prepare_udt_generated_names()
2109
3754
  # Pre-scan for strategy series vars
2110
3755
  self._prescan_strategy_series()
2111
3756
  self._security_ohlc_hist_fields_by_sec: dict[int, set[str]] = {}
@@ -2115,6 +3760,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2115
3760
  # request.security helper-call results read at a history offset
2116
3761
  # (``myHelper()[k]``). Maps (sec_id, node-id) -> backing Series metadata.
2117
3762
  self._security_expr_hist_by_node: dict[tuple[int, int], dict] = {}
3763
+ self._prepare_lazy_saturated_roc3_sites()
2118
3764
 
2119
3765
  lines: list[str] = []
2120
3766
 
@@ -2125,14 +3771,197 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2125
3771
  # 1. Includes
2126
3772
  self._emit_includes(lines)
2127
3773
 
2128
- # 1b. UDT structs
2129
- # Drawing field names per struct are pre-computed in __init__ as
2130
- # ``self._udt_omitted_fields`` so visit_expr / visit_stmt can
2131
- # consult the same map. Drawing types: label, line, box, table,
2132
- # linefill, polyline, chart.point. These have no backtest runtime
2133
- # representation in PineForge — see pineforge-codegen issue #10.
2134
- for type_name, fields in self._udt_defs.items():
3774
+ # 1b. User-defined object handles and backing records.
3775
+ #
3776
+ # Pine UDT values are object IDs: ordinary assignment aliases the same
3777
+ # object, while ``copy()`` allocates one detached outer object and keeps
3778
+ # nested UDT/map/matrix IDs shallow-shared. Direct array-valued fields
3779
+ # still use std::vector value storage, so their built-in UDT copy path
3780
+ # is rejected explicitly until arrays gain handle identity. Emit every
3781
+ # authored type as a small nullable handle first, then its value record.
3782
+ # The two-phase order also permits self/nested UDT fields without
3783
+ # embedding a C++ type recursively by value.
3784
+ for type_name in self._udt_defs:
2135
3785
  lines.append(f"struct {type_name} {{")
3786
+ lines.append(" int32_t __pf_id = -1;")
3787
+ lines.append("};")
3788
+ lines.append(
3789
+ f"inline bool is_na(const {type_name}& _z) "
3790
+ "{ return _z.__pf_id < 0; }"
3791
+ )
3792
+ lines.append("")
3793
+
3794
+ if self._udt_defs:
3795
+ lines.extend([
3796
+ "template <typename _PFValue>",
3797
+ f"struct {self._checkpoint_traits_cpp_name};",
3798
+ "",
3799
+ f"class {self._udt_undo_coordinator_cpp_name} {{",
3800
+ " std::vector<std::function<void()>> _pf_undo_;",
3801
+ " uint64_t _pf_generation_ = 0;",
3802
+ " bool _pf_active_ = false;",
3803
+ "public:",
3804
+ " struct Snapshot { uint64_t generation; };",
3805
+ f" {self._udt_undo_coordinator_cpp_name}() = default;",
3806
+ f" {self._udt_undo_coordinator_cpp_name}(",
3807
+ f" const {self._udt_undo_coordinator_cpp_name}&) = delete;",
3808
+ f" {self._udt_undo_coordinator_cpp_name}& operator=(",
3809
+ f" const {self._udt_undo_coordinator_cpp_name}&) = delete;",
3810
+ f" {self._udt_undo_coordinator_cpp_name}(",
3811
+ f" {self._udt_undo_coordinator_cpp_name}&&) = delete;",
3812
+ f" {self._udt_undo_coordinator_cpp_name}& operator=(",
3813
+ f" {self._udt_undo_coordinator_cpp_name}&&) = delete;",
3814
+ "",
3815
+ " Snapshot snapshot() {",
3816
+ " if (_pf_generation_ == std::numeric_limits<uint64_t>::max()) {",
3817
+ ' throw std::overflow_error("UDT checkpoint generation exhausted");',
3818
+ " }",
3819
+ " ++_pf_generation_;",
3820
+ " _pf_undo_.clear();",
3821
+ " _pf_active_ = true;",
3822
+ " return Snapshot{_pf_generation_};",
3823
+ " }",
3824
+ " uint64_t generation() const { return _pf_generation_; }",
3825
+ " bool active() const { return _pf_active_; }",
3826
+ " bool empty() const { return _pf_undo_.empty(); }",
3827
+ " void record(uint64_t generation, std::function<void()> undo) {",
3828
+ " if (!_pf_active_ || generation != _pf_generation_) {",
3829
+ ' throw std::runtime_error("invalid UDT undo generation");',
3830
+ " }",
3831
+ " _pf_undo_.push_back(std::move(undo));",
3832
+ " }",
3833
+ " void restore(const Snapshot& snapshot) {",
3834
+ " if (!_pf_active_ || snapshot.generation != _pf_generation_) {",
3835
+ ' throw std::runtime_error("invalid UDT coordinator checkpoint token");',
3836
+ " }",
3837
+ " for (auto entry = _pf_undo_.rbegin();",
3838
+ " entry != _pf_undo_.rend(); ++entry) (*entry)();",
3839
+ " _pf_undo_.clear();",
3840
+ " }",
3841
+ "};",
3842
+ "",
3843
+ "template <typename _PFHandle, typename _PFRecord>",
3844
+ f"class {self._udt_arena_template_cpp_name} {{",
3845
+ f" using _PFRecordTraits = {self._checkpoint_traits_cpp_name}<_PFRecord>;",
3846
+ " using _PFRecordSnapshot = typename _PFRecordTraits::snapshot_type;",
3847
+ " struct _PFSlot {",
3848
+ " _PFRecord value;",
3849
+ " uint64_t logged_generation = 0;",
3850
+ " };",
3851
+ " std::deque<_PFSlot> _pf_records_;",
3852
+ f" {self._udt_undo_coordinator_cpp_name}* _pf_coordinator_;",
3853
+ " std::size_t _pf_checkpoint_size_ = 0;",
3854
+ " uint64_t _pf_checkpoint_generation_ = 0;",
3855
+ " bool _pf_checkpoint_active_ = false;",
3856
+ "",
3857
+ " void capture(std::size_t index) {",
3858
+ " if (!_pf_checkpoint_active_) return;",
3859
+ " auto& slot = _pf_records_.at(index);",
3860
+ " if (slot.logged_generation",
3861
+ " == _pf_checkpoint_generation_) return;",
3862
+ " auto snapshot = _PFRecordTraits::take(slot.value);",
3863
+ " _pf_coordinator_->record(_pf_checkpoint_generation_,",
3864
+ " [this, index, snapshot = std::move(snapshot)]() mutable {",
3865
+ " auto& restore_slot = _pf_records_.at(index);",
3866
+ " _PFRecordTraits::restore(restore_slot.value, snapshot);",
3867
+ " restore_slot.logged_generation = 0;",
3868
+ " });",
3869
+ " slot.logged_generation = _pf_checkpoint_generation_;",
3870
+ " }",
3871
+ "public:",
3872
+ " struct Snapshot {",
3873
+ " uint64_t generation;",
3874
+ " std::size_t size;",
3875
+ " };",
3876
+ "",
3877
+ f" explicit {self._udt_arena_template_cpp_name}(",
3878
+ f" {self._udt_undo_coordinator_cpp_name}* coordinator)",
3879
+ " : _pf_coordinator_(coordinator) {",
3880
+ " if (!_pf_coordinator_)",
3881
+ ' throw std::invalid_argument("UDT arena requires undo coordinator");',
3882
+ " }",
3883
+ f" {self._udt_arena_template_cpp_name}(",
3884
+ f" const {self._udt_arena_template_cpp_name}&) = delete;",
3885
+ f" {self._udt_arena_template_cpp_name}& operator=(",
3886
+ f" const {self._udt_arena_template_cpp_name}&) = delete;",
3887
+ f" {self._udt_arena_template_cpp_name}(",
3888
+ f" {self._udt_arena_template_cpp_name}&&) = delete;",
3889
+ f" {self._udt_arena_template_cpp_name}& operator=(",
3890
+ f" {self._udt_arena_template_cpp_name}&&) = delete;",
3891
+ "",
3892
+ " _PFHandle create(_PFRecord value) {",
3893
+ " if (_pf_records_.size() > static_cast<std::size_t>(",
3894
+ " std::numeric_limits<int32_t>::max())) {",
3895
+ ' throw std::length_error("UDT object-ID capacity exceeded");',
3896
+ " }",
3897
+ " const auto id = static_cast<int32_t>(_pf_records_.size());",
3898
+ " _pf_records_.push_back(_PFSlot{std::move(value), 0});",
3899
+ " return _PFHandle{id};",
3900
+ " }",
3901
+ " _PFHandle copy(_PFHandle value) {",
3902
+ " return create(static_cast<const "
3903
+ f"{self._udt_arena_template_cpp_name}&>(*this).get(value));",
3904
+ " }",
3905
+ " _PFRecord& get(_PFHandle value) {",
3906
+ " if (value.__pf_id < 0",
3907
+ " || static_cast<std::size_t>(value.__pf_id) >= _pf_records_.size()) {",
3908
+ ' throw std::runtime_error("UDT access on na or invalid object ID");',
3909
+ " }",
3910
+ " const auto index = static_cast<std::size_t>(value.__pf_id);",
3911
+ " capture(index);",
3912
+ " return _pf_records_[index].value;",
3913
+ " }",
3914
+ " const _PFRecord& get(_PFHandle value) const {",
3915
+ " if (value.__pf_id < 0",
3916
+ " || static_cast<std::size_t>(value.__pf_id) >= _pf_records_.size()) {",
3917
+ ' throw std::runtime_error("UDT access on na or invalid object ID");',
3918
+ " }",
3919
+ " return _pf_records_[static_cast<std::size_t>(value.__pf_id)].value;",
3920
+ " }",
3921
+ " const _PFRecord& read(_PFHandle value) const {",
3922
+ " return get(value);",
3923
+ " }",
3924
+ " std::size_t size() const { return _pf_records_.size(); }",
3925
+ " _PFRecord& record_at(std::size_t index) {",
3926
+ " capture(index);",
3927
+ " return _pf_records_.at(index).value;",
3928
+ " }",
3929
+ " const _PFRecord& record_at(std::size_t index) const {",
3930
+ " return _pf_records_.at(index).value;",
3931
+ " }",
3932
+ " Snapshot snapshot() {",
3933
+ " if (!_pf_coordinator_->active()) {",
3934
+ ' throw std::runtime_error("UDT coordinator checkpoint is not active");',
3935
+ " }",
3936
+ " _pf_checkpoint_generation_ = _pf_coordinator_->generation();",
3937
+ " _pf_checkpoint_size_ = _pf_records_.size();",
3938
+ " _pf_checkpoint_active_ = true;",
3939
+ " return Snapshot{_pf_checkpoint_generation_,",
3940
+ " _pf_checkpoint_size_};",
3941
+ " }",
3942
+ " void restore(const Snapshot& snapshot) {",
3943
+ " if (!_pf_checkpoint_active_",
3944
+ " || snapshot.generation != _pf_checkpoint_generation_",
3945
+ " || snapshot.generation != _pf_coordinator_->generation()",
3946
+ " || snapshot.size != _pf_checkpoint_size_",
3947
+ " || _pf_records_.size() < snapshot.size",
3948
+ " || !_pf_coordinator_->empty()) {",
3949
+ ' throw std::runtime_error("invalid UDT checkpoint token");',
3950
+ " }",
3951
+ " _pf_records_.resize(snapshot.size);",
3952
+ " }",
3953
+ "};",
3954
+ "",
3955
+ ])
3956
+
3957
+ # Drawing field names per record are pre-computed in __init__ as
3958
+ # ``self._udt_omitted_fields`` so visit_expr / visit_stmt can consult
3959
+ # the same map. Authored field defaults are evaluated by ``Type.new``
3960
+ # at the call site; record declarations use only inert typed defaults so
3961
+ # bar/TA expressions never leak into namespace-scope C++ initializers.
3962
+ for type_name, fields in self._udt_defs.items():
3963
+ record_type = self._udt_record_cpp_type(type_name)
3964
+ lines.append(f"struct {record_type} {{")
2136
3965
  field_specs = self._udt_field_type_specs.get(type_name, {})
2137
3966
  omitted = self._udt_omitted_fields.get(type_name, set())
2138
3967
  for f in fields:
@@ -2140,28 +3969,16 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2140
3969
  continue
2141
3970
  spec = field_specs.get(f.name) or self._type_spec_from_hint_name(f.type_name)
2142
3971
  cpp_type = self._type_spec_to_cpp(spec)
2143
- # Pine ``int`` is 64-bit (it routinely holds UNIX-ms timestamps
2144
- # and large bar indices); emit UDT int fields as ``int64_t`` so a
2145
- # field initialised from ``time``/``current_bar_.timestamp`` does
2146
- # not truncate / narrow-init.
2147
3972
  if cpp_type == "int":
2148
3973
  cpp_type = "int64_t"
2149
- if f.default:
2150
- default = self._visit_expr(f.default)
2151
- else:
2152
- default = self._default_for_spec(spec)
3974
+ default = self._default_for_spec(spec)
2153
3975
  lines.append(f" {cpp_type} {f.name} = {default};")
2154
- # NA sentinel (always the last data member). A default-constructed
2155
- # UDT - ``var T x = na``, an array fill slot, ``T.copy()`` no-arg -
2156
- # is na; the ``T.new(...)`` lowering sets this false. This lets
2157
- # ``na(udtVar)`` lower to the ``is_na(const T&)`` overload below
2158
- # instead of failing because no ``is_na`` accepts a struct.
2159
- lines.append(f" bool __pf_na = true;")
2160
- lines.append(f" static {type_name} create() {{ return {type_name}{{}}; }}")
2161
3976
  lines.append("};")
2162
- lines.append(f"inline bool is_na(const {type_name}& _z) {{ return _z.__pf_na; }}")
2163
3977
  lines.append("")
2164
3978
 
3979
+ if self._uses_map or self._uses_matrix or self._udt_defs:
3980
+ self._emit_handle_checkpoint_traits(lines)
3981
+
2165
3982
  # 1c. Enum constants + string tables for str.tostring(enumVar)
2166
3983
  for enum_name, members in self._enum_defs.items():
2167
3984
  for i, member in enumerate(members):
@@ -2176,15 +3993,41 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2176
3993
  )
2177
3994
  lines.append("")
2178
3995
 
3996
+ # Source-shaped lazy ROC call clocks are generated support types, not
3997
+ # script state themselves. Their per-callsite instances are declared
3998
+ # below inside GeneratedStrategy and therefore join the automatic COOF
3999
+ # checkpoint inventory.
4000
+ self._emit_lazy_saturated_roc3_helper(lines)
4001
+
2179
4002
  # 2. Open class
2180
4003
  lines.append("class GeneratedStrategy : public BacktestEngine {")
2181
4004
  lines.append("public:")
2182
4005
  _script_state_decl_start = len(lines)
4006
+
4007
+ # One strategy-local undo coordinator gives every UDT arena a single
4008
+ # reverse first-touch order. This is required when records from distinct
4009
+ # authored UDT types shallow-share the same map or matrix identity.
4010
+ if self._udt_defs:
4011
+ lines.append(
4012
+ f" {self._udt_undo_coordinator_cpp_name} "
4013
+ f"{self._udt_undo_coordinator_member_name};"
4014
+ )
4015
+
4016
+ # Per-type object stores precede every handle-bearing script member.
4017
+ # COOF rollback therefore replays the coordinator, truncates arenas,
4018
+ # then rebinds ordinary variables, arrays and matrices in that order.
4019
+ for type_name in self._udt_defs:
4020
+ lines.append(
4021
+ f" {self._udt_arena_cpp_type(type_name)} "
4022
+ f"{self._udt_arena_member_name(type_name)}"
4023
+ f"{{&{self._udt_undo_coordinator_member_name}}};"
4024
+ )
2183
4025
 
2184
4026
  # request.security state
2185
4027
  for item in self._security_calls:
2186
4028
  sec_id = item["sec_id"]
2187
4029
  expr_node = item["expr_node"]
4030
+ self._validate_security_persistent_var_control_flow(expr_node)
2188
4031
  returns_tuple = item.get("returns_tuple", False)
2189
4032
  tuple_size = item.get("tuple_size", 0)
2190
4033
  if item.get("is_lower_tf_array"):
@@ -2234,15 +4077,33 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2234
4077
  if ctype == "std::vector<double>":
2235
4078
  lines.append(f" {ctype} _req_sec_{sec_id}_{i}{{}};")
2236
4079
  else:
2237
- lines.append(f" {ctype} _req_sec_{sec_id}_{i} = na<double>();")
4080
+ default = {
4081
+ "double": "na<double>()",
4082
+ "bool": "false",
4083
+ "int": "0",
4084
+ "std::string": 'std::string("")',
4085
+ }.get(ctype, self._default_for_type(ctype))
4086
+ lines.append(
4087
+ f" {ctype} _req_sec_{sec_id}_{i} = {default};"
4088
+ )
2238
4089
  elif returns_tuple and tuple_size and tuple_size > 0:
2239
4090
  self._security_ohlc_hist_fields_by_sec[sec_id] = (
2240
4091
  self._collect_security_ohlc_hist_fields_for_call(item)
2241
4092
  )
2242
4093
  site = self._get_ta_site(expr_node)
2243
4094
  ta_name = self._ta_name_from_site(site) if site is not None else ""
2244
- ctype = TA_TUPLE_RESULT_TYPES.get(ta_name, "std::tuple<double, double>")
2245
- default = self._security_tuple_result_default(ctype, tuple_size)
4095
+ ctype = TA_TUPLE_RESULT_TYPES.get(
4096
+ ta_name,
4097
+ self._security_helper_tuple_cpp_type(
4098
+ tuple_size,
4099
+ item.get("tuple_element_types", ()),
4100
+ ),
4101
+ )
4102
+ default = self._security_tuple_result_default(
4103
+ ctype,
4104
+ tuple_size,
4105
+ item.get("tuple_element_types", ()),
4106
+ )
2246
4107
  lines.append(f" {ctype} _req_sec_{sec_id} = {default};")
2247
4108
  else:
2248
4109
  self._security_ohlc_hist_fields_by_sec[sec_id] = (
@@ -2292,6 +4153,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2292
4153
  lines.append(f" std::vector<{vtype}> _precalc_{site.member_name};")
2293
4154
  lines.append(" bool _use_precalc = false;")
2294
4155
 
4156
+ for clock_name in self._lazy_saturated_roc3_clock_by_node.values():
4157
+ lines.append(
4158
+ f" {self._lazy_saturated_roc3_type_name} {clock_name};"
4159
+ )
4160
+ if self._lazy_saturated_roc3_clock_by_node:
4161
+ # Dedicated eager close[3] fallback. Its fixed four-slot capacity
4162
+ # is independent of the user's max_bars_back directive, which may
4163
+ # legitimately be smaller than the offset this generated route
4164
+ # requires. It is ordinary copyable script state and therefore
4165
+ # joins the automatic COOF checkpoint below.
4166
+ lines.append(
4167
+ " Series<double> "
4168
+ f"{self._lazy_saturated_roc3_history_name}{{4}};"
4169
+ )
4170
+
2295
4171
  # Security evaluator TA members (cloned from expression dependencies)
2296
4172
  # Skip for user function call expressions — their TA deps are internal to the function
2297
4173
  for info in self._security_eval_info:
@@ -2311,6 +4187,22 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2311
4187
  continue
2312
4188
  seen_var_members.add(name)
2313
4189
  safe = self._safe_name(name)
4190
+ callable_collection_spec = (
4191
+ None
4192
+ if name in self._global_collection_types
4193
+ else self._callable_var_collection_spec(name)
4194
+ )
4195
+ if callable_collection_spec is not None:
4196
+ lines.append(
4197
+ f" {self._type_spec_to_cpp(callable_collection_spec)} {safe};"
4198
+ )
4199
+ continue
4200
+ callable_udt_spec = self._callable_var_udt_spec(name)
4201
+ if callable_udt_spec is not None:
4202
+ lines.append(
4203
+ f" {self._type_spec_to_cpp(callable_udt_spec)} {safe};"
4204
+ )
4205
+ continue
2314
4206
  # Detect array vars from init expression. Guard the substring
2315
4207
  # heuristic against a UDT constructor that merely WRAPS array.new /
2316
4208
  # array.from in its arguments — e.g.
@@ -2321,12 +4213,24 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2321
4213
  _is_udt_ctor_init = any(
2322
4214
  _init_str_s.startswith(f"{u}.new") for u in self._udt_defs
2323
4215
  )
2324
- if (not _is_udt_ctor_init) and (
2325
- "array.new" in _init_str_s or "array.from" in _init_str_s
2326
- or name in self._array_vars
4216
+ exact_member_spec = self._global_collection_types.get(name)
4217
+ exact_array_member = (
4218
+ exact_member_spec is not None
4219
+ and exact_member_spec.kind == "array"
4220
+ )
4221
+ if exact_array_member or (
4222
+ not _is_udt_ctor_init
4223
+ and (
4224
+ "array.new" in _init_str_s
4225
+ or "array.from" in _init_str_s
4226
+ or name in self._array_vars
4227
+ )
2327
4228
  ):
2328
4229
  self._array_vars.add(name)
2329
- lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(name))} {safe};")
4230
+ array_spec = exact_member_spec or self._array_spec_for_name(name)
4231
+ lines.append(
4232
+ f" {self._type_spec_to_cpp(array_spec)} {safe};"
4233
+ )
2330
4234
  continue
2331
4235
  # Detect matrix vars from init expression OR from the set
2332
4236
  # populated by ``_register_global_aggregate_member_types``
@@ -2334,7 +4238,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2334
4238
  # not just ``matrix.new``).
2335
4239
  if name in self._matrix_specs:
2336
4240
  pass # already registered upstream
2337
- elif "matrix.new" in str(init_str):
4241
+ elif not _is_udt_ctor_init and "matrix.new" in str(init_str):
2338
4242
  self._matrix_specs[name] = TypeSpec.matrix(TypeSpec.primitive("float"))
2339
4243
  self._collection_types[name] = self._matrix_specs[name]
2340
4244
  if name in self._matrix_specs:
@@ -2343,9 +4247,17 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2343
4247
  if "ta.pivot_point_levels" in str(init_str):
2344
4248
  lines.append(f" std::vector<double> {safe};")
2345
4249
  continue
2346
- if "map.new" in str(init_str) or name in self._map_vars:
4250
+ exact_map_member = (
4251
+ exact_member_spec is not None
4252
+ and exact_member_spec.kind == "map"
4253
+ )
4254
+ if exact_map_member or (
4255
+ not _is_udt_ctor_init
4256
+ and ("map.new" in str(init_str) or name in self._map_vars)
4257
+ ):
2347
4258
  self._map_vars.add(name)
2348
- lines.append(f" {self._type_spec_to_cpp(self._map_spec_for_name(name))} {safe};")
4259
+ map_spec = exact_member_spec or self._map_spec_for_name(name)
4260
+ lines.append(f" {self._type_spec_to_cpp(map_spec)} {safe};")
2349
4261
  continue
2350
4262
  # Detect UDT vars. Two signals: (1) the analyzer recorded an
2351
4263
  # explicit UDT type annotation in ``_udt_var_types`` - this is the
@@ -2359,14 +4271,18 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2359
4271
  # C++ handle struct (Series<Line> when also history-referenced).
2360
4272
  # Drawing names are NOT in _udt_defs, so the udt branch below would
2361
4273
  # self-zero them to double; handle them first.
2362
- _draw_cpp = DRAWING_TYPE_TO_CPP.get(self._udt_var_types.get(name))
4274
+ member_udt_type = self._member_udt_type(name)
4275
+ _draw_cpp = (
4276
+ self._drawing_var_member_cpp_types.get(name)
4277
+ or DRAWING_TYPE_TO_CPP.get(member_udt_type)
4278
+ )
2363
4279
  if _draw_cpp is not None:
2364
- if name in self.ctx.series_vars:
4280
+ if safe in self._series_var_member_names:
2365
4281
  lines.append(f" Series<{_draw_cpp}> {safe}{_mbb};")
2366
4282
  else:
2367
4283
  lines.append(f" {_draw_cpp} {safe};")
2368
4284
  continue
2369
- udt_type = self._udt_var_types.get(name)
4285
+ udt_type = member_udt_type
2370
4286
  if udt_type not in self._udt_defs:
2371
4287
  udt_type = None
2372
4288
  if udt_type is None:
@@ -2382,14 +4298,35 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2382
4298
  # (time/time_close/timestamp), otherwise the na sentinel narrows.
2383
4299
  if cpp_type == "int" and self._is_int64_builtin_init(name):
2384
4300
  cpp_type = "int64_t"
2385
- if name in self.ctx.series_vars:
4301
+ # Persistent declarations have collision-safe exact member names
4302
+ # (for example ``x`` and ``x__blk1`` for sibling lexical blocks).
4303
+ # ``ctx.series_vars`` is only the legacy raw-name union, so using it
4304
+ # here can invert the storage types: the unreferenced sibling gets
4305
+ # Series<T> while the exact history-referenced sibling stays a
4306
+ # scalar. Exact analyzer identity must win for every persistent
4307
+ # primitive, just as it already does for drawing handles above.
4308
+ if safe in self._series_var_member_names:
2386
4309
  lines.append(f" Series<{cpp_type}> {safe}{_mbb};")
2387
4310
  else:
2388
- lines.append(f" {cpp_type} {safe};")
4311
+ if name in self._runtime_scalar_var_init_members:
4312
+ # A conditional declaration may not execute for many bars,
4313
+ # while COOF rollback still value-copies every member. Give
4314
+ # pending runtime vars a typed Pine-na sentinel so that copy
4315
+ # is always defined; the declaration-site guard overwrites
4316
+ # it on the first actual execution.
4317
+ pending = self._typed_na_init("na<double>()", name, ptype)
4318
+ lines.append(f" {cpp_type} {safe} = {pending};")
4319
+ else:
4320
+ lines.append(f" {cpp_type} {safe};")
2389
4321
 
2390
4322
  # 6. Non-var series vars
2391
4323
  for name in sorted(self.ctx.series_vars):
2392
- if name not in self._var_names:
4324
+ if (name not in self._var_names
4325
+ or name in self._nonpersistent_series_decl_names):
4326
+ if (name in self._qualified_func_var_raw_names
4327
+ and name in self._ordinary_global_binding_names
4328
+ and name not in self._ordinary_global_series_names):
4329
+ continue
2393
4330
  safe = self._safe_name(name)
2394
4331
  cpp_type = self._series_type_for(name)
2395
4332
  lines.append(f" Series<{cpp_type}> {safe}{_mbb};")
@@ -2398,7 +4335,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2398
4335
  for _fi_idx, site in enumerate(self.ctx.fixnan_sites):
2399
4336
  if _fi_idx in self._dead_fixnan_indices:
2400
4337
  continue
2401
- cpp_type = PINE_TYPE_TO_CPP.get(site.pine_type, "double")
4338
+ cpp_type = self._fixnan_site_cpp_type(site)
2402
4339
  lines.append(f" {cpp_type} {site.member_name} = na<{cpp_type}>();")
2403
4340
 
2404
4341
  # 8. Strategy series (e.g., strategy.closedtrades[1])
@@ -2423,13 +4360,14 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2423
4360
  # (so user-defined functions can reference them)
2424
4361
  seen_global = set()
2425
4362
  for name, ptype in self.ctx.global_var_decls:
2426
- if name in seen_global or name in self.ctx.series_vars or name in self._var_names:
2427
- continue
2428
- # De-hoisted UDT array-element alias (Pine reference semantics): the
2429
- # in-loop VarDecl is emitted as a fresh ``UDT& z = arr[i];`` local
2430
- # reference each iteration, so there is no persistent class member.
2431
- if name in self._udt_array_get_ref_locals:
2432
- seen_global.add(name)
4363
+ exact_scalar_override = (
4364
+ name in self._qualified_func_var_raw_names
4365
+ and name in self._ordinary_global_binding_names
4366
+ and name not in self._ordinary_global_series_names
4367
+ )
4368
+ if (name in seen_global
4369
+ or (name in self.ctx.series_vars and not exact_scalar_override)
4370
+ or name in self._var_names):
2433
4371
  continue
2434
4372
  seen_global.add(name)
2435
4373
  safe = self._safe_name(name)
@@ -2444,12 +4382,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2444
4382
  "localPivots", "securityPivotPointsArray", "pivotPointsArray",
2445
4383
  ):
2446
4384
  lines.append(f" std::vector<double> {safe} = std::vector<double>();")
2447
- elif name in self._udt_var_types:
4385
+ elif self._member_udt_type(name) is not None:
2448
4386
  # Non-var global of UDT type — declare as the struct so
2449
4387
  # downstream method dispatch works. Probes:
2450
4388
  # data/validation/udt-method-probe-19-array-of-udt-method,
2451
4389
  # data/validation/udt-method-probe-20-udt-return-from-func.
2452
- udt_t = self._udt_var_types[name]
4390
+ udt_t = self._member_udt_type(name)
2453
4391
  # Drawing handle global (L-N6 / U): map line/box/label/linefill
2454
4392
  # to the C++ handle struct (the default is na, id=-1).
2455
4393
  _draw_cpp = DRAWING_TYPE_TO_CPP.get(udt_t)
@@ -2459,7 +4397,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2459
4397
  lines.append(f" {udt_t} {safe} = {udt_t}{{}};")
2460
4398
  else:
2461
4399
  expr = self.ctx.global_expr_map.get(name) if hasattr(self.ctx, "global_expr_map") else None
2462
- cpp_type = self._infer_type(expr) if expr is not None else PINE_TYPE_TO_CPP.get(ptype, "double")
4400
+ if (
4401
+ name in self._direct_program_tuple_binding_names
4402
+ and ptype == PineType.BOOL
4403
+ ):
4404
+ # request.security homogeneous-bool helper tuples carry
4405
+ # their exact element family in analyzer metadata. The
4406
+ # coarse request.security call expression itself still
4407
+ # reports FLOAT, so prefer that exact binding type here.
4408
+ cpp_type = "bool"
4409
+ else:
4410
+ cpp_type = (
4411
+ self._infer_type(expr)
4412
+ if expr is not None
4413
+ else PINE_TYPE_TO_CPP.get(ptype, "double")
4414
+ )
2463
4415
  default = self._default_for_type(cpp_type)
2464
4416
  lines.append(f" {cpp_type} {safe} = {default};")
2465
4417
 
@@ -2473,16 +4425,20 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2473
4425
  if cloned_safe in emitted_clones:
2474
4426
  continue # already declared by another function's clone
2475
4427
  emitted_clones.add(cloned_safe)
2476
- self._emit_cloned_var_decl(orig_safe, cloned_safe, _mbb, lines)
4428
+ self._emit_cloned_var_decl(
4429
+ orig_safe, cloned_safe, _mbb, lines, owner_func=fname
4430
+ )
2477
4431
 
2478
4432
  # 8c2. Fresh var members for context-sensitive helper instances (nested
2479
4433
  # helpers reached through >1 distinct call path). Each fresh instance
2480
4434
  # gets its OWN scalar/series state so two paths never collide.
2481
- for orig_safe, fresh_safe in self._fresh_var_members:
4435
+ for owner_func, orig_safe, fresh_safe in self._fresh_var_members:
2482
4436
  if fresh_safe in emitted_clones:
2483
4437
  continue
2484
4438
  emitted_clones.add(fresh_safe)
2485
- self._emit_cloned_var_decl(orig_safe, fresh_safe, _mbb, lines)
4439
+ self._emit_cloned_var_decl(
4440
+ orig_safe, fresh_safe, _mbb, lines, owner_func=owner_func
4441
+ )
2486
4442
 
2487
4443
  # 8c3. Fresh fixnan members for context-sensitive helper instances.
2488
4444
  # Each fresh instance gets its OWN previous-value member so two
@@ -2491,7 +4447,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2491
4447
  if fresh_safe in emitted_clones:
2492
4448
  continue
2493
4449
  emitted_clones.add(fresh_safe)
2494
- cpp_type = PINE_TYPE_TO_CPP.get(orig_site.pine_type, "double")
4450
+ cpp_type = self._fixnan_site_cpp_type(orig_site)
2495
4451
  lines.append(f" {cpp_type} {fresh_safe} = na<{cpp_type}>();")
2496
4452
 
2497
4453
  # 8d. Drawing-objects-as-data arenas (gated on _uses_drawing so
@@ -2509,39 +4465,59 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2509
4465
  if self.ctx.var_members:
2510
4466
  lines.append(" bool _var_initialized = false;")
2511
4467
 
2512
- # 9a. Per-function-variant ``var`` init flags. A function-scoped
2513
- # ``var`` (Pine "init once" semantics) is a function-local static:
2514
- # its initializer runs on the FIRST call to that function variant
2515
- # (with the first bar's values the function actually sees) and the
2516
- # result persists for the strategy's lifetime. Each clone (cs0,
2517
- # cs1, ...) is an independent instance with its own flag.
2518
- # ``func_var_members`` is keyed by the plain Pine function name
2519
- # (``fi.name``), so this matches both plain UDFs and UDT methods.
4468
+ # 9a. Per-member flags for primitive runtime ``var`` / ``varip``
4469
+ # initializers. Unlike the global aggregate/Series latch above, these
4470
+ # live at the declaration site so prior statements are available and a
4471
+ # conditional declaration initializes on its first actual execution.
4472
+ emitted_runtime_flags: set[str] = set()
4473
+ for flag in self._runtime_var_init_flags.values():
4474
+ if flag in emitted_runtime_flags:
4475
+ continue
4476
+ emitted_runtime_flags.add(flag)
4477
+ lines.append(f" bool {flag} = false;")
4478
+
4479
+ # 9b. Legacy per-variant latch members are retained for generated-C++
4480
+ # and checkpoint-shape compatibility. Callable initializers no longer
4481
+ # consult them: every initialized declaration owns an exact-site flag.
4482
+ # Keep their historical spelling when free, but avoid legal authored
4483
+ # ``_fvinit_*`` members and every natural/fresh clone name.
4484
+ used_legacy_flags = set(self._runtime_var_init_flag_used_names)
4485
+
4486
+ def allocate_legacy_flag(base: str) -> str:
4487
+ flag = base
4488
+ suffix = 2
4489
+ while flag in used_legacy_flags:
4490
+ flag = f"{base}_{suffix}"
4491
+ suffix += 1
4492
+ used_legacy_flags.add(flag)
4493
+ return flag
4494
+
2520
4495
  for fi in self.ctx.func_infos:
2521
4496
  if fi.name not in self.ctx.func_var_members:
2522
4497
  continue
4498
+ emitted = self._func_cpp_base_name(fi.name)
2523
4499
  total_cs = self.ctx.func_call_site_counts.get(fi.name, 0)
2524
- if total_cs > 0:
2525
- for cs_idx in range(total_cs):
2526
- lines.append(f" bool _fvinit_{self._func_safe_name(fi.name)}_cs{cs_idx} = false;")
2527
- else:
2528
- lines.append(f" bool _fvinit_{self._func_safe_name(fi.name)} = false;")
4500
+ variants = range(total_cs) if total_cs > 0 else (None,)
4501
+ for cs_idx in variants:
4502
+ suffix = f"_cs{cs_idx}" if cs_idx is not None else ""
4503
+ flag = allocate_legacy_flag(f"_fvinit_{emitted}{suffix}")
4504
+ lines.append(f" bool {flag} = false;")
2529
4505
 
2530
- # 9a2. ``var`` init flags for fresh context-sensitive helper instances.
2531
4506
  for inst in self._fresh_instances:
2532
4507
  if inst["fname"] in self.ctx.func_var_members and inst["var_remap"]:
2533
- lines.append(f" bool _fvinit_{inst['name']} = false;")
4508
+ flag = allocate_legacy_flag(f"_fvinit_{inst['name']}")
4509
+ lines.append(f" bool {flag} = false;")
2534
4510
 
2535
- # 9b. _ta_initialized_ flag for runtime TA re-sizing (first on_bar only).
4511
+ # 9c. _ta_initialized_ flag for runtime TA re-sizing (first on_bar only).
2536
4512
  if self.ctx.ta_call_sites:
2537
4513
  lines.append(" bool _ta_initialized_ = false;")
2538
4514
 
2539
- # 9c. _inputs_initialized_ flag for cached global inputs.
4515
+ # 9d. _inputs_initialized_ flag for cached global inputs.
2540
4516
  lines.append(" bool _inputs_initialized_ = false;")
2541
4517
 
2542
4518
  lines.append("")
2543
4519
 
2544
- # 9d. Historical execution rollback checkpoint. Derive the member
4520
+ # 9e. Historical execution rollback checkpoint. Derive the member
2545
4521
  # inventory from the declarations above so every future generated
2546
4522
  # state category is captured automatically (or generation fails loudly
2547
4523
  # if it introduces an unfamiliar declaration form).
@@ -2645,8 +4621,26 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2645
4621
 
2646
4622
  # _get_ta_site / _ta_member_name / _ta_name_from_site / _TA_IMPLICIT_REPLACE
2647
4623
  # / _ta_compute_args_for_site / _security_ta_compute_args_for_site /
2648
- # _if_body_has_ta / _is_result_assignment / _expr_contains_ta /
2649
- # _hoist_if_body live on TaSiteHelper (codegen/ta.py).
4624
+ def _resolve_ta_ctor_arg(self, arg_str: str) -> str:
4625
+ """Resolve one TA constructor argument without widening const folding.
4626
+
4627
+ ``var`` values deliberately stay out of ``_known_vars`` because normal
4628
+ expression reads must still target persistent storage. A uniquely
4629
+ bound, never-reassigned top-level integer literal is nevertheless a
4630
+ faithful static TA buffer length. Admit only that exact bare alias;
4631
+ arithmetic, input/timeframe expressions, shadows, and all mutable or
4632
+ history-bearing bindings continue through the existing resolver/reset
4633
+ guardrails unchanged.
4634
+ """
4635
+ resolved = self._resolve_known(arg_str)
4636
+ if self._is_compile_time_value(resolved):
4637
+ return resolved
4638
+ if (
4639
+ arg_str in self._stable_var_ctor_literals
4640
+ and not self._known_var_is_lexically_shadowed(arg_str)
4641
+ ):
4642
+ return str(self._stable_var_ctor_literals[arg_str].value)
4643
+ return resolved
2650
4644
 
2651
4645
  def _resolve_known(self, arg_str: str) -> str:
2652
4646
  """Resolve a string arg, replacing known var names with their values.
@@ -2657,7 +4651,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2657
4651
  if arg_str == "na":
2658
4652
  return "na<double>()"
2659
4653
  # Direct variable lookup
2660
- if arg_str in self._known_vars:
4654
+ if (arg_str in self._known_vars
4655
+ and not self._known_var_is_lexically_shadowed(arg_str)):
2661
4656
  val = self._known_vars[arg_str]
2662
4657
  if isinstance(val, bool):
2663
4658
  return "true" if val else "false"
@@ -2674,6 +4669,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2674
4669
  resolved = arg_str
2675
4670
  # Sort by length (longest first) to avoid partial replacements
2676
4671
  for name in sorted(self._known_vars, key=len, reverse=True):
4672
+ if self._known_var_is_lexically_shadowed(name):
4673
+ continue
2677
4674
  val = self._known_vars[name]
2678
4675
  if isinstance(val, (int, float)):
2679
4676
  import re
@@ -2744,15 +4741,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2744
4741
  """
2745
4742
  if not isinstance(node, MemberAccess):
2746
4743
  return False
2747
- # Cheap path: receiver is a bare identifier we already track in
2748
- # ``_udt_var_types`` (the common case — ``m.tag``, ``s.ln``).
2749
- if isinstance(node.object, Identifier):
2750
- udt_name = self._udt_var_types.get(node.object.name)
2751
- if udt_name is None:
2752
- return False
2753
- return node.member in self._udt_omitted_fields.get(udt_name, ())
2754
- # General path: try to infer the receiver's UDT type via the same
2755
- # spec-resolver visit_expr uses for fallback member access.
4744
+ # Resolve even bare identifiers through the lexical/exact TypeSpec
4745
+ # path. The legacy raw-name UDT registry can describe an unrelated
4746
+ # callable local and therefore cannot safely drive field omission.
2756
4747
  recv_spec = self._type_spec_from_expr(node.object)
2757
4748
  if recv_spec is not None and recv_spec.kind == "udt" and recv_spec.name:
2758
4749
  return node.member in self._udt_omitted_fields.get(recv_spec.name, ())
@@ -2792,7 +4783,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2792
4783
  "exp": "std::exp", "pow": "std::pow",
2793
4784
  "sin": "std::sin", "cos": "std::cos", "tan": "std::tan",
2794
4785
  "asin": "std::asin", "acos": "std::acos", "atan": "std::atan",
2795
- "sign": "(double)([] (double _v) { return (_v>0) - (_v<0); })",
4786
+ "sign": _MATH_SIGN_CPP_FN,
2796
4787
  }
2797
4788
 
2798
4789
  # Pine logical operators (word form) -> C++ operator, used when rendering
@@ -2917,7 +4908,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2917
4908
  return s
2918
4909
  def _rep(p: re.Match) -> str:
2919
4910
  nm = p.group(0)
2920
- if nm in self._derived_input_expr and nm not in seen:
4911
+ if (nm in self._derived_input_expr
4912
+ and nm not in seen
4913
+ and not self._known_var_is_lexically_shadowed(nm)):
2921
4914
  inner = self._derived_input_expr[nm]
2922
4915
  return "(" + _expand_derived(inner, seen | {nm}, depth + 1) + ")"
2923
4916
  return nm
@@ -2926,6 +4919,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2926
4919
  expanded = _expand_derived(arg_str)
2927
4920
 
2928
4921
  tokens = set(ident_re.findall(expanded))
4922
+ if any(
4923
+ self._known_var_is_lexically_shadowed(name)
4924
+ for name in tokens
4925
+ ):
4926
+ return None
2929
4927
 
2930
4928
  # Gate: every identifier token must be renderable. If any token is an
2931
4929
  # unrecognised bare identifier (not an input, not a known const, not
@@ -2947,7 +4945,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2947
4945
  # Known compile-time consts that survived expansion (e.g. ``pi`` was
2948
4946
  # NOT tracked as a Python value but its name token is a stable var
2949
4947
  # already; pure numeric names are in _known_vars and covered above).
2950
- leftover = {t for t in leftover if t not in self._known_vars}
4948
+ leftover = {
4949
+ t for t in leftover
4950
+ if (t not in self._known_vars
4951
+ or self._known_var_is_lexically_shadowed(t))
4952
+ }
2951
4953
  # Inline ``input(...)`` / ``input.<t>(...)`` calls (a bare input
2952
4954
  # expression passed straight as a length arg, e.g.
2953
4955
  # ``adx(input(15), input(15))``) are re-parsed and rendered below,
@@ -3018,6 +5020,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3018
5020
  # reads, and inline known compile-time consts (non-input) as literals.
3019
5021
  def _sub(p: re.Match) -> str:
3020
5022
  name = p.group(0)
5023
+ if self._known_var_is_lexically_shadowed(name):
5024
+ return name
3021
5025
  if name in self._input_backed_vars:
3022
5026
  call_node = self._input_var_to_call.get(name)
3023
5027
  if call_node is None:
@@ -3025,7 +5029,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3025
5029
  func_name_i, namespace_i = self._resolve_callee(call_node.callee)
3026
5030
  title = self._get_input_title(call_node, var_name=name)
3027
5031
  return self._render_input_value(call_node, func_name_i, namespace_i, title)
3028
- if name in self._known_vars and name not in self._input_backed_vars:
5032
+ if (name in self._known_vars
5033
+ and name not in self._input_backed_vars
5034
+ and not self._known_var_is_lexically_shadowed(name)):
3029
5035
  val = self._known_vars[name]
3030
5036
  if isinstance(val, bool):
3031
5037
  return "true" if val else "false"
@@ -3091,7 +5097,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3091
5097
  return rendered
3092
5098
 
3093
5099
 
3094
- def _collect_ta_runtime_resets(self) -> list[str]:
5100
+ def _collect_ta_runtime_resets(
5101
+ self,
5102
+ *,
5103
+ security_source_node: ASTNode | None = None,
5104
+ ) -> list[str]:
3095
5105
  """Collect reassignment statements for every TA object whose ctor args
3096
5106
  depend on an input-backed variable. Returned strings are raw C++
3097
5107
  assignment statements (no enclosing block/indent). Empty list when no
@@ -3115,7 +5125,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3115
5125
  runtime_args.append(rt)
3116
5126
  any_runtime = True
3117
5127
  else:
3118
- resolved = self._resolve_known(a)
5128
+ resolved = self._resolve_ta_ctor_arg(a)
3119
5129
  runtime_args.append(resolved if self._is_compile_time_value(resolved) else "1")
3120
5130
  if any_runtime:
3121
5131
  resets.append(
@@ -3140,6 +5150,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3140
5150
  sec_cs_idx = (sec_item or {}).get("callsite_idx")
3141
5151
  for idx, variants in (info.get("ta_variants") or {}).items():
3142
5152
  site = self.ctx.ta_call_sites[idx]
5153
+ if (
5154
+ security_source_node is not None
5155
+ and site.node is not security_source_node
5156
+ ):
5157
+ continue
3143
5158
  ctor_site = site
3144
5159
  if sec_containing and sec_cs_idx is not None:
3145
5160
  remap = self._func_cs_ta_remap.get((sec_containing, sec_cs_idx))
@@ -3151,18 +5166,55 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3151
5166
  ctor_site = cand
3152
5167
  if not ctor_site.ctor_args:
3153
5168
  continue
3154
- runtime_args = []
3155
- any_runtime = False
3156
- for a in ctor_site.ctor_args:
3157
- rt = self._runtime_ctor_arg_for_reset(a)
3158
- if rt is not None:
3159
- runtime_args.append(rt)
3160
- any_runtime = True
3161
- else:
3162
- resolved = self._resolve_known(a)
3163
- runtime_args.append(resolved if self._is_compile_time_value(resolved) else "1")
3164
- if any_runtime:
3165
- for variant in variants:
5169
+ for variant in variants:
5170
+ ctor_args, ctor_arg_stability = self._security_ta_ctor_args_for_variant(
5171
+ info["sec_id"],
5172
+ site,
5173
+ variant.get("binding_stack", ()),
5174
+ fallback_args=ctor_site.ctor_args,
5175
+ )
5176
+ runtime_args = []
5177
+ any_runtime = False
5178
+ for arg_pos, a in enumerate(ctor_args):
5179
+ rt = self._runtime_ctor_arg_for_reset(a)
5180
+ resolved = self._resolve_ta_ctor_arg(a)
5181
+ lowered_variant = ctor_arg_stability is not None
5182
+ stable_variant_arg = (
5183
+ lowered_variant and ctor_arg_stability[arg_pos]
5184
+ )
5185
+ if (
5186
+ rt is None
5187
+ and stable_variant_arg
5188
+ and not self._is_compile_time_value(resolved)
5189
+ ):
5190
+ # This is trusted C++ from _build_security_expr and
5191
+ # its fully helper-bound AST passed _expr_is_stable.
5192
+ rt = a
5193
+ if (
5194
+ rt is None
5195
+ and lowered_variant
5196
+ and not self._is_compile_time_value(resolved)
5197
+ ):
5198
+ self._codegen_error(
5199
+ getattr(site, "node", None),
5200
+ f"Unsupported requested-context TA constructor "
5201
+ f"length '{a}' for {site.class_name}: the "
5202
+ "helper-bound expression is not a stable "
5203
+ "per-run scalar.",
5204
+ hint=("Use a literal, an input.*() value, "
5205
+ "timeframe.* metadata, or arithmetic "
5206
+ "over those for TA lengths."),
5207
+ )
5208
+ if rt is not None:
5209
+ runtime_args.append(rt)
5210
+ any_runtime = True
5211
+ else:
5212
+ runtime_args.append(
5213
+ resolved
5214
+ if self._is_compile_time_value(resolved)
5215
+ else "1"
5216
+ )
5217
+ if any_runtime:
3166
5218
  resets.append(
3167
5219
  f"{variant['member_name']} = {site.class_name}({', '.join(runtime_args)});"
3168
5220
  )