@pineforge/codegen-pyodide 0.10.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +16 -16
  2. package/glue.py +24 -16
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +125 -34
  5. package/pineforge_codegen/analyzer/__init__.py +2 -0
  6. package/pineforge_codegen/analyzer/base.py +754 -76
  7. package/pineforge_codegen/analyzer/call_handlers.py +260 -40
  8. package/pineforge_codegen/analyzer/contracts.py +37 -0
  9. package/pineforge_codegen/analyzer/diagnostics.py +30 -4
  10. package/pineforge_codegen/analyzer/tables.py +49 -8
  11. package/pineforge_codegen/analyzer/types.py +33 -1
  12. package/pineforge_codegen/ast_nodes.py +32 -1
  13. package/pineforge_codegen/block_locals.py +185 -0
  14. package/pineforge_codegen/builtin_keywords.py +42 -0
  15. package/pineforge_codegen/codegen/base.py +896 -156
  16. package/pineforge_codegen/codegen/constant_fold.py +131 -0
  17. package/pineforge_codegen/codegen/drawing.py +221 -79
  18. package/pineforge_codegen/codegen/emit_top.py +946 -213
  19. package/pineforge_codegen/codegen/helpers.py +435 -14
  20. package/pineforge_codegen/codegen/host_members.py +162 -0
  21. package/pineforge_codegen/codegen/input.py +252 -85
  22. package/pineforge_codegen/codegen/security.py +4372 -377
  23. package/pineforge_codegen/codegen/session_market.py +71 -0
  24. package/pineforge_codegen/codegen/ta.py +1188 -100
  25. package/pineforge_codegen/codegen/tables.py +193 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +1882 -78
  28. package/pineforge_codegen/codegen/visit_call.py +920 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +738 -57
  30. package/pineforge_codegen/codegen/visit_stmt.py +595 -49
  31. package/pineforge_codegen/external_requests.py +877 -0
  32. package/pineforge_codegen/lexer.py +104 -22
  33. package/pineforge_codegen/library_inline.py +1304 -0
  34. package/pineforge_codegen/library_modules.py +126 -0
  35. package/pineforge_codegen/library_v5.py +683 -0
  36. package/pineforge_codegen/limits.py +138 -0
  37. package/pineforge_codegen/method_binding.py +33 -0
  38. package/pineforge_codegen/parser.py +384 -68
  39. package/pineforge_codegen/pine_libraries.py +266 -0
  40. package/pineforge_codegen/pine_spelling.py +216 -0
  41. package/pineforge_codegen/pragmas.py +64 -10
  42. package/pineforge_codegen/security_contexts.py +1585 -0
  43. package/pineforge_codegen/session_reads.py +84 -0
  44. package/pineforge_codegen/signatures.py +48 -23
  45. package/pineforge_codegen/support_checker.py +1106 -85
  46. package/pineforge_codegen-1.0.0.tar.gz +0 -0
  47. package/release.json +2 -2
  48. package/tables.json +23 -21
  49. package/transpile.worker.mjs +24 -16
  50. package/pineforge_codegen-0.10.4.tar.gz +0 -0
@@ -25,10 +25,18 @@ from ..analyzer import (
25
25
  TA_MULTI_CTOR,
26
26
  TA_NO_CTOR,
27
27
  TA_PERIOD_ARG,
28
+ TA_LENGTH_ONLY_DEFAULT_SOURCE,
28
29
  )
29
30
  from ..symbols import PineType, TypeSpec, method_receiver_type_name
30
31
  from .. import signatures as sigs
31
32
  from ..errors import CompileError, Diagnostic, Level, Phase, SourceLocation
33
+ from ..limits import TimeBudget, iter_ast_nodes
34
+ from ..session_reads import emitted_session_reads
35
+ from ..block_locals import block_declarations, decl_key
36
+ from ..pine_spelling import (
37
+ blank_string_literals, input_call_spans, pine_string_literal,
38
+ spell_input_call, sub_identifiers,
39
+ )
32
40
 
33
41
 
34
42
  @dataclass(frozen=True)
@@ -79,6 +87,41 @@ from .tables import (
79
87
  _merge_kwargs,
80
88
  )
81
89
 
90
+ # A UDT field of array type. Pine arrays are references: ``T.new(a)`` holds
91
+ # the array ``a`` itself, not a copy. The field aliases a script ``var``
92
+ # array its record is built from (its storage lives as long as the strategy
93
+ # and keeps its identity across bars), and owns, by move or copy, any other.
94
+ # Copying the field shares what it holds, as Pine's shallow ``copy()`` does.
95
+ UDT_ARRAY_FIELD_CPP = r"""
96
+ template <typename T>
97
+ class _PFArrayField {
98
+ public:
99
+ _PFArrayField()
100
+ : owned_(std::make_shared<std::vector<T>>()), data_(owned_.get()) {}
101
+ _PFArrayField(const std::vector<T>& value)
102
+ : owned_(std::make_shared<std::vector<T>>(value)), data_(owned_.get()) {}
103
+ _PFArrayField(std::vector<T>&& value)
104
+ : owned_(std::make_shared<std::vector<T>>(std::move(value))),
105
+ data_(owned_.get()) {}
106
+ static _PFArrayField alias(std::vector<T>& value) {
107
+ return _PFArrayField(&value);
108
+ }
109
+ std::vector<T>& operator*() const { return *data_; }
110
+ const std::shared_ptr<std::vector<T>>& owned() const { return owned_; }
111
+ std::vector<T>* data() const { return data_; }
112
+ void rebind(std::shared_ptr<std::vector<T>> owned, std::vector<T>* data) {
113
+ owned_ = std::move(owned);
114
+ data_ = data;
115
+ }
116
+
117
+ private:
118
+ explicit _PFArrayField(std::vector<T>* data) : data_(data) {}
119
+
120
+ std::shared_ptr<std::vector<T>> owned_;
121
+ std::vector<T>* data_ = nullptr;
122
+ };
123
+ """
124
+
82
125
  TA_TUPLE_RESULT_TYPES = {
83
126
  "macd": "ta::MACDResult",
84
127
  "supertrend": "ta::SupertrendResult",
@@ -86,6 +129,7 @@ TA_TUPLE_RESULT_TYPES = {
86
129
  "bb": "ta::BBResult",
87
130
  "kc": "ta::KCResult",
88
131
  "vwap_bands": "ta::VWAPBandsResult",
132
+ "vwap_anchored_bands": "ta::VWAPBandsResult",
89
133
  }
90
134
 
91
135
  # (TA_IMPLICIT_COMPUTE / TA_COMPUTE_ARGS now imported from .tables above.)
@@ -101,7 +145,12 @@ TA_TUPLE_RESULT_TYPES = {
101
145
 
102
146
  # CPP_RESERVED + the NamingHelper mixin are pulled in from helpers.py so the
103
147
  # small naming/walk utilities can be shared with future visitor mixins.
104
- from .helpers import CPP_RESERVED, NamingHelper
148
+ from .helpers import (
149
+ CPP_RESERVED, INLINE_HISTORY_KINDS, SESSION_FLAG_MEMBERS, NamingHelper,
150
+ cpp_code_only, na_preserving_int_cast, pine_truth_cast,
151
+ )
152
+ from .constant_fold import fold_numeric_expression
153
+ from .session_market import SESSION_MARKET_CPP, SESSION_MARKET_MEMBER
105
154
 
106
155
  # TypeInferer mixin owns the ~15 type-spec / C++-type inference helpers
107
156
  # previously scattered across this module; see ``codegen/types.py``.
@@ -109,8 +158,7 @@ from .types import TypeInferer
109
158
 
110
159
  # TaSiteHelper owns site lookup, .compute() arg construction, and the TA
111
160
  # call-site machinery. The runtime-reset chain (_resolve_known and friends)
112
- # stays on CodeGen for now because it relies on Python's compile-time
113
- # expression evaluator.
161
+ # stays on CodeGen for now because it shares the constructor constant folder.
114
162
  from .ta import TaSiteHelper
115
163
 
116
164
  # InputHelper owns Pine input.* analysis (defaults, titles, getter dispatch,
@@ -179,12 +227,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
179
227
  _prescan_strategy_series), and the runtime-reset chain
180
228
  (_resolve_known / _is_skip_expr / _runtime_ctor_arg_for_reset /
181
229
  _collect_ta_runtime_resets / _emit_ta_runtime_reset) — kept here
182
- because the chain relies on Python's compile-time expression
183
- evaluator.
230
+ because the chain shares the constructor constant folder.
184
231
  """
185
232
 
186
- def __init__(self, ctx: AnalyzerContext) -> None:
233
+ # True while emitting the body of a function or method inlined from a
234
+ # v5 library (``library_v5``): and/or evaluate both operands, a for
235
+ # loop's end is fixed before its first iteration, a negative array
236
+ # index stops the run, and an observer of v5's na bool is refused.
237
+ _pine_v5_body = False
238
+
239
+ def __init__(self, ctx: AnalyzerContext,
240
+ budget: TimeBudget | None = None) -> None:
187
241
  self.ctx = ctx
242
+ self._budget = budget
243
+ self._budget_visit_count = 0
244
+ self._initialise_safe_names(ctx.ast)
188
245
  # Lexical Pine names remain in ``ctx.func_var_members``. This overlay
189
246
  # carries exact class-member identities only for collision-qualified
190
247
  # ordinary FuncDefs (identity mappings for every other ordinary UDF).
@@ -310,11 +367,57 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
310
367
  # Set of var/series member names that belong to user functions (need cloning)
311
368
  self._func_var_members_set: set[str] = set()
312
369
  self._precalc_loop_active: bool = False
370
+ # Nonzero while the expression visitor lowers a request.security
371
+ # payload (``_build_security_expr``): the session helper below reads
372
+ # the chart's timeframe, which does not describe the security bar.
373
+ self._security_payload_depth: int = 0
374
+ # The request.security builder's context while its expression visitor
375
+ # fallback lowers a payload node: a user call, TA site, helper-bound
376
+ # name or requested-bar history the visitor reaches under that node is
377
+ # handed back to the builder (``_security_fallback_delegate``).
378
+ self._security_fallback_frame: dict | None = None
379
+ # Whether the evaluator being emitted lowers its payload's user calls
380
+ # under builtin calls, and its typed methods, on the requested bar;
381
+ # whether it did, and the first thing it left on the chart's terms
382
+ # (``_emit_security_evaluator_requested``).
383
+ self._security_requested_calls: bool = True
384
+ self._security_requested_used: bool = False
385
+ self._security_chart_read: tuple | None = None
386
+ # Evaluators that keep every earlier build's lowering, decided while
387
+ # a method's TA constructor arguments were lowered: sec_id -> reason.
388
+ self._security_chart_evaluators: dict[int, tuple] = {}
389
+ # request.security helper series of string values (``var string``, a
390
+ # string local read with history) live in their own map; the member
391
+ # is declared when a payload's helper holds one.
392
+ self._security_string_series_declared: bool = False
393
+ self._security_string_series: set[str] = set()
394
+ # Each request.security evaluator local's C++ type (a string local
395
+ # takes ``na<std::string>()``; the builder's ``/`` reads it).
396
+ self._security_local_cpp_types: dict[str, str] = {}
397
+ # C++ the security builder hands a synthetic name: a helper-local
398
+ # history index lowered in its helper's scope.
399
+ self._security_raw_cpp: dict[str, str] = {}
400
+ # Set while a request.security TA history index is lowered: an input
401
+ # it reads is its override-aware getter, since the evaluator can run
402
+ # before on_bar initializes the input members.
403
+ self._security_index_inputs: bool = False
404
+ # A source input's selected series read at a history offset in a
405
+ # payload (``_security_bar_history_field``): the payload-method
406
+ # pre-pass below reads it before generate() starts the run's own.
407
+ self._security_source_hist_fields: dict[tuple[str, str], tuple] = {}
408
+ # Set when a chart expression calls ``_pf_session_market_``; its type
409
+ # and member are emitted once the whole TU is lowered.
410
+ self._uses_session_market: bool = False
411
+ # session.* reads inside a request.security payload already warned.
412
+ self._warned_security_session_sites: set[int] = set()
313
413
  # Top-level lazy-edge TA sites hoisted to every-bar evaluation for the
314
414
  # statement currently being lowered: FuncCall id -> local name, and
315
415
  # Subscript id -> ``_hist_call_*`` member (see ``ta.py``).
316
416
  self._hoisted_ta_values: dict[int, str] = {}
317
417
  self._hoisted_hist_reads: dict[int, str] = {}
418
+ # ``*`` nodes a ``%`` or ``/`` reads: an int product among them is
419
+ # computed in 64 bits (visit_expr._lower_binop).
420
+ self._wide_int_products: set[int] = set()
318
421
  # Names of ``var`` members that live in a callable scope (not global).
319
422
  # Their exact declaration statements own initialization; they must not
320
423
  # be initialized by the constructor or the global on_bar preamble.
@@ -712,6 +815,29 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
712
815
  self._fixnan_site_map[id(_fsite.node)] = _fsite
713
816
  # Track strategy series vars (e.g., strategy.closedtrades[1])
714
817
  self._strategy_series_vars: set[str] = set()
818
+ # session.* flags read at an offset at the top level (their per-bar
819
+ # Series): _prescan_session_history.
820
+ self._session_history_flags: set[str] = set()
821
+ self._session_call_flags: dict[str, set[str]] = {}
822
+ self._session_call_owner: dict[int, str] = {}
823
+ self._session_history_member_names: dict[str, str] = {}
824
+ # The names a rendered read may not take (the script's own first: a
825
+ # request.security timeframe renders here, before the prescans), and
826
+ # each read refused where it renders: its stand-in name, then the
827
+ # read (kept alive, so its id stays its own), where to report it, why,
828
+ # and the uncloned function it asks to clone, if any; raised or asked
829
+ # only if the C++ emits the stand-in (_settle_session_reads).
830
+ self._session_names_used: set[str] = set(self._safe_name_occupied)
831
+ self._refused_session_read_names: dict[tuple[int, str], str] = {}
832
+ self._refused_session_reads: dict[
833
+ str, tuple[ASTNode, ASTNode, str, str | None]] = {}
834
+ # The uncloned functions whose reads the emitted C++ holds: another
835
+ # analysis clones them (pineforge_codegen._generate).
836
+ self.session_functions_needing_clones: frozenset[str] = frozenset()
837
+ # The top-level block declarations whose type the member of their name
838
+ # cannot hold (``block_locals``): transpile() names each apart and
839
+ # runs again.
840
+ self.block_locals_needing_names: frozenset = frozenset()
715
841
  # Track global-scope non-var declarations (emitted as class members)
716
842
  self._global_member_vars: set[str] = set()
717
843
  for name, _ in ctx.global_var_decls:
@@ -816,6 +942,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
816
942
  # for-in loop iterator names (must resolve member access, not enum fallback)
817
943
  self._current_loop_vars: set[str] = set()
818
944
  self._current_loop_var_specs: dict[str, "TypeSpec"] = {}
945
+ # The counted loops' binders among them (``for (int i = ...)``).
946
+ self._current_counted_loop_vars: set[str] = set()
819
947
  # Track array variables for codegen
820
948
  self._array_vars: set[str] = set()
821
949
  # Track map variables for codegen
@@ -852,9 +980,20 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
852
980
  _h = _hints[_i] if _i < len(_hints) else None
853
981
  if _h and str(_h).replace(" ", "") in _SKIP_DECL_TYPES:
854
982
  self._visual_drop_vars.add(_p)
983
+ # Build set of all member names (series vars, var members) for collision
984
+ # detection. It precedes the security metadata below: a timeframe
985
+ # computed by a user function (``tf = tfFromLabel(choice)``) renders
986
+ # that call through ``_func_safe_name`` at registration time.
987
+ self._all_member_names: set[str] = set()
988
+ for name in ctx.series_vars:
989
+ self._all_member_names.add(self._safe_name(name))
990
+ for name, _, _ in ctx.var_members:
991
+ self._all_member_names.add(self._safe_name(name))
855
992
  # Collect request.security metadata per call
856
993
  self._security_eval_info: list[dict] = []
857
994
  self._security_ta_variant_names: dict[tuple[int, int, tuple], str] = {}
995
+ # Reassigned globals a timeframe reads (``_security_tf_replay_prologue``).
996
+ self._security_tf_mutable_reads: set[str] = set()
858
997
  for item in self._security_calls:
859
998
  sec_id = item["sec_id"]
860
999
  tf_node = item["tf_node"]
@@ -865,8 +1004,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
865
1004
  # Resolve the timeframe: a literal/const/global gives a static tf;
866
1005
  # a function-parameter tf is resolved from the call sites (the
867
1006
  # evaluator is a class method, so the param is not in scope there).
868
- tf_str, tf_expr = self._resolve_security_tf(
869
- tf_node, item.get("containing_func", ""))
1007
+ # A lower-timeframe request has no chart-timeframe fallback.
1008
+ self._security_tf_lower = bool(item.get("is_lower_tf_array"))
1009
+ if item.get("dead"):
1010
+ # A helper no top-level statement reaches: its evaluator is
1011
+ # never read, registered on the chart timeframe.
1012
+ tf_str, tf_expr = None, "input_tf_"
1013
+ else:
1014
+ tf_str, tf_expr = self._resolve_security_tf(
1015
+ tf_node, item.get("containing_func", ""))
870
1016
 
871
1017
  is_lookahead_on = False
872
1018
  if lookahead_node is not None:
@@ -879,7 +1025,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
879
1025
  is_gaps_on = True
880
1026
 
881
1027
  expr_node = item["expr_node"]
882
- inline_helper_ta_indices: set[int] = set()
1028
+ inline_helper_ta_indices: set[tuple] = set()
883
1029
  ta_binding_stacks = self._collect_security_ta_binding_stacks(
884
1030
  expr_node,
885
1031
  inline_ta_indices=inline_helper_ta_indices,
@@ -889,7 +1035,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
889
1035
  for idx in sorted(ta_indices):
890
1036
  site = self.ctx.ta_call_sites[idx]
891
1037
  binding_map = ta_binding_stacks.get(idx) or {(): ()}
892
- signatures = sorted(binding_map.keys(), key=repr)
1038
+ signatures = sorted(
1039
+ binding_map.keys(),
1040
+ key=lambda sig: self._security_variant_order_key(sig, binding_map[sig]),
1041
+ )
893
1042
  use_base_name = len(signatures) == 1
894
1043
  variants: list[dict] = []
895
1044
  for variant_idx, signature in enumerate(signatures):
@@ -926,17 +1075,19 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
926
1075
  "ta_indices": sorted(ta_indices),
927
1076
  "ta_binding_stacks": ta_binding_stacks,
928
1077
  "ta_variants": ta_variants,
929
- "inline_helper_ta_indices": sorted(inline_helper_ta_indices),
1078
+ # (index, signature) of each TA variant computed inline, and
1079
+ # (index, _SECURITY_THROUGH_GLOBAL) of a global's site a
1080
+ # multi-statement helper reads, which the prologue computes.
1081
+ "inline_helper_ta_indices": sorted(inline_helper_ta_indices, key=repr),
930
1082
  "depends_on_mutable_globals": item.get("depends_on_mutable_globals", False),
931
1083
  "mutable_globals": list(item.get("mutable_globals", [])),
932
1084
  "is_lower_tf_array": bool(item.get("is_lower_tf_array", False)),
1085
+ # Another symbol's feed (``_emit_foreign_security_registration``);
1086
+ # a helper nothing reaches keeps the chart registration.
1087
+ "foreign": bool(item.get("foreign")) and not item.get("dead"),
1088
+ "symbol_node": item.get("symbol_node"),
1089
+ "ignore_invalid_node": item.get("ignore_invalid_node"),
933
1090
  })
934
- # Build set of all member names (series vars, var members) for collision detection
935
- self._all_member_names: set[str] = set()
936
- for name in ctx.series_vars:
937
- self._all_member_names.add(self._safe_name(name))
938
- for name, _, _ in ctx.var_members:
939
- self._all_member_names.add(self._safe_name(name))
940
1091
  self._register_global_aggregate_member_types()
941
1092
  self._uses_map = self._detect_map_usage()
942
1093
  self._uses_matrix = self._detect_matrix_usage()
@@ -998,10 +1149,36 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
998
1149
  return True
999
1150
  return False
1000
1151
 
1152
+ def _series_var_init_keeps_preamble(self, init_str: str, init_ast,
1153
+ top_level: bool = True) -> bool:
1154
+ """Whether a history-read ``var``'s first value can be pushed in the
1155
+ first-bar preamble, ahead of the script body: a top-level declaration
1156
+ of a constant, a string literal, ``na`` or a bar field (the bar is in
1157
+ place by then). Any other initializer reads what the body computes
1158
+ before the declaration on the first bar -- a global (``b = close *
1159
+ 2``), a UDT field (``direction.neutral``), a call, an input's
1160
+ override -- which the preamble read before it was assigned, or
1161
+ spelled as raw Pine that did not compile
1162
+ (``signal.push(direction.neutral)``); and a declaration in a block
1163
+ initializes on the first bar that reaches it, which the preamble's
1164
+ bar 0 is not."""
1165
+ if not top_level:
1166
+ return False
1167
+ if init_ast is None:
1168
+ return True
1169
+ if self._scalar_var_init_depends_on_runtime_input(init_ast):
1170
+ return False
1171
+ if isinstance(init_ast, StringLiteral):
1172
+ return True
1173
+ if isinstance(init_ast, Identifier) and init_ast.name in BAR_FIELDS:
1174
+ return True
1175
+ return self._is_compile_time_value(self._resolve_known(init_str))
1176
+
1001
1177
  def _is_runtime_scalar_var_initializer(
1002
1178
  self, name: str, ptype, init_str: str, init_ast,
1003
1179
  drawing_cpp: str | None = None,
1004
- is_series: bool = False) -> bool:
1180
+ is_series: bool = False,
1181
+ top_level: bool = True) -> bool:
1005
1182
  """Return True for a persistent primitive that must init in execution.
1006
1183
 
1007
1184
  Global Series and aggregate state keep their specialized preamble
@@ -1025,7 +1202,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1025
1202
  is_series
1026
1203
  or not self._is_na_expr(init_ast)
1027
1204
  )
1028
- if is_series and name in self.ctx.series_vars:
1205
+ if is_series and self._series_var_init_keeps_preamble(init_str, init_ast, top_level):
1029
1206
  return False
1030
1207
  udt_type = self._member_udt_type(name)
1031
1208
  if udt_type in self._udt_defs:
@@ -1041,6 +1218,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1041
1218
  return (
1042
1219
  not self._is_compile_time_value(ctor_val)
1043
1220
  or self._scalar_var_init_depends_on_runtime_input(init_ast)
1221
+ # A history-read var in a block (its member may be renamed,
1222
+ # ``k__blk1``): its history is na until the first bar that
1223
+ # reaches it (``_series_var_init_keeps_preamble``).
1224
+ or (is_series and not top_level)
1044
1225
  )
1045
1226
 
1046
1227
  def _prepare_runtime_scalar_var_initializers(self) -> None:
@@ -1166,6 +1347,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1166
1347
  top_level_node_ids = {id(stmt) for stmt in self.ctx.ast.body}
1167
1348
  for node_id, meta in metadata_by_node.items():
1168
1349
  stmt, member_name, ptype, init_str, is_callable_scoped = meta
1350
+ if self._budget is not None:
1351
+ # Each declaration rescans every declaration.
1352
+ self._budget.check(stmt.loc, Phase.CODEGEN)
1169
1353
  if not isinstance(stmt, VarDecl) or not (stmt.is_var or stmt.is_varip):
1170
1354
  continue
1171
1355
  stmt_spec = type_specs_by_node.get(node_id)
@@ -1223,7 +1407,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1223
1407
  nullable_collection_selection_decl_site
1224
1408
  or self._is_runtime_scalar_var_initializer(
1225
1409
  member_name, ptype, init_str, stmt.value, drawing_cpp,
1226
- is_series
1410
+ is_series, node_id in top_level_node_ids
1227
1411
  )
1228
1412
  )
1229
1413
  )
@@ -1280,8 +1464,29 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1280
1464
  # var-member metadata. Record their exact source declaration type so
1281
1465
  # functions can resolve globals without consulting the analyzer's
1282
1466
  # legacy raw-name UDT registry (which later locals may overwrite).
1283
- for stmt in self.ctx.ast.body:
1284
- if not isinstance(stmt, VarDecl) or stmt.is_var or stmt.is_varip:
1467
+ # A declaration in a top-level block (``if c`` / ``for``) is hoisted
1468
+ # to the same class-member storage, so it is recorded too: its
1469
+ # ``cond ? box.new(...) : na`` needs the handle type for the ``na``.
1470
+ def top_level_scope_decls(stmts):
1471
+ for stmt in stmts:
1472
+ if isinstance(stmt, VarDecl):
1473
+ yield stmt
1474
+ elif isinstance(stmt, IfStmt):
1475
+ yield from top_level_scope_decls(stmt.body)
1476
+ yield from top_level_scope_decls(stmt.else_body)
1477
+ elif isinstance(stmt, (ForStmt, ForInStmt, WhileStmt)):
1478
+ yield from top_level_scope_decls(stmt.body)
1479
+ elif isinstance(stmt, SwitchStmt):
1480
+ for _case, body in stmt.cases:
1481
+ yield from top_level_scope_decls(body)
1482
+ yield from top_level_scope_decls(stmt.default_body)
1483
+
1484
+ top_level_ids = {id(stmt) for stmt in self.ctx.ast.body}
1485
+ for stmt in top_level_scope_decls(self.ctx.ast.body):
1486
+ if stmt.is_var or stmt.is_varip:
1487
+ continue
1488
+ if (id(stmt) not in top_level_ids
1489
+ and stmt.name in self._global_drawing_cpp_types):
1285
1490
  continue
1286
1491
  spec = (
1287
1492
  self._type_spec_from_hint_name(stmt.type_hint)
@@ -1362,10 +1567,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1362
1567
  return
1363
1568
 
1364
1569
  func_bodies: dict[str, list] = {}
1570
+ func_params: dict[str, set[str]] = {}
1365
1571
  for fi in ctx.func_infos:
1366
1572
  node = getattr(fi, "node", None)
1367
1573
  if node is not None and getattr(node, "body", None):
1368
1574
  func_bodies.setdefault(fi.name, node.body)
1575
+ func_params.setdefault(fi.name, set(getattr(node, "params", ()) or ()))
1369
1576
 
1370
1577
  # Pine forbids recursive callable execution. Most scalar-only cycles
1371
1578
  # never enter this state-instance pass (and some legacy dead-branch
@@ -1426,10 +1633,30 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1426
1633
  return list(self._func_cs_ta_remap.get((fname, 0), {}).keys())
1427
1634
 
1428
1635
  def var_originals(fname: str) -> list[str]:
1429
- return [
1636
+ names = [
1430
1637
  self._safe_name(self._func_var_storage_name(fname, n))
1431
1638
  for n, _, _ in ctx.func_var_members.get(fname, [])
1432
1639
  ]
1640
+ # A history-read local (``float f = 0.0`` then ``f := ... f[1]``)
1641
+ # is persistent Series state too. Without it a fresh instance
1642
+ # kept writing the original member, so every call path it served
1643
+ # shared one series (cs-lev's second f_pole path fed its nine
1644
+ # true-range filters into the price filter's ``_f``).
1645
+ # Same storage rule as the natural csN remap of the callee's own
1646
+ # series vars (``orig_names`` above).
1647
+ params = func_params.get(fname, set())
1648
+ for sv in sorted(ctx.func_series_vars.get(fname, ())):
1649
+ if sv in params:
1650
+ continue # a history-read parameter is the caller's series
1651
+ exact = self._func_var_storage_name(fname, sv)
1652
+ storage = self._safe_name(
1653
+ exact
1654
+ if self._safe_name(exact) in self._series_var_member_names
1655
+ else sv
1656
+ )
1657
+ if storage not in names:
1658
+ names.append(storage)
1659
+ return names
1433
1660
 
1434
1661
  def fixnan_originals(fname: str) -> list[str]:
1435
1662
  return list(self._func_cs_fixnan_remap.get((fname, 0), {}).keys())
@@ -1581,6 +1808,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1581
1808
  "name": inst_name,
1582
1809
  "fresh": True,
1583
1810
  "call_site_idx": None,
1811
+ # The written call this instance runs, for its
1812
+ # parameter and return types: an untyped parameter
1813
+ # takes the argument that call passes, as the
1814
+ # call's own cs{j} clone does.
1815
+ "type_call_site_idx": j,
1584
1816
  "ta_remap": composed_ta,
1585
1817
  "var_remap": fvar_remap,
1586
1818
  "fixnan_remap": ffixnan_remap,
@@ -2269,10 +2501,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2269
2501
  all_args.append(None)
2270
2502
  all_args[i] = node.kwargs[pname]
2271
2503
 
2272
- if func_name == "highest" and len(all_args) == 1:
2273
- all_args = [Identifier(name="high"), all_args[0]]
2274
- elif func_name == "lowest" and len(all_args) == 1:
2275
- all_args = [Identifier(name="low"), all_args[0]]
2504
+ # The one-arg length forms read high / low (as the analyzer's merge).
2505
+ default_source = TA_LENGTH_ONLY_DEFAULT_SOURCE.get(func_name)
2506
+ if default_source is not None and (
2507
+ len(all_args) == 1 or (len(all_args) == 2 and all_args[0] is None)):
2508
+ all_args = [Identifier(name=default_source), all_args[-1]]
2276
2509
 
2277
2510
  return all_args
2278
2511
 
@@ -2316,6 +2549,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2316
2549
  rejected by the constructor guard. Names are NOT folded into
2317
2550
  ``_known_vars`` (no use-site inlining): only the length-analysis path is
2318
2551
  affected, and the ``var`` member still emits and initializes normally.
2552
+
2553
+ The exception is ``var v = input.*()`` itself, which is registered like
2554
+ the plain binding ``v = input.*()`` (``_collect_known_var``).
2319
2555
  """
2320
2556
  literal_candidates = self._admitted_stable_var_ctor_literals(reassigned)
2321
2557
 
@@ -2329,6 +2565,19 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2329
2565
  continue
2330
2566
  if self._decl_binding_is_series(id(stmt), stmt.name):
2331
2567
  continue
2568
+ # A never-reassigned ``var v = input.*()`` holds the input's value
2569
+ # on every bar, as ``v = input.*()`` does, so it is registered the
2570
+ # same way: an input-backed name read through its input call. The
2571
+ # TA runtime reset then keys the input by
2572
+ # ``_get_input_title(call, var_name=v)``, the key the member reads
2573
+ # it by. Recorded below as a derived expression, the reset re-read
2574
+ # the call's own spelling instead: an untitled input under "" and
2575
+ # without its keyword arguments, so an override never resized the
2576
+ # indicator.
2577
+ if self._is_var_input_binding(stmt):
2578
+ self._collect_known_var(stmt)
2579
+ if stmt.name in self._input_var_to_call:
2580
+ continue
2332
2581
  if stmt.value is None or not self._expr_is_stable(stmt.value):
2333
2582
  continue
2334
2583
  expr_str = self._arith_expr_to_str(stmt.value)
@@ -2340,9 +2589,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2340
2589
  self._stable_var_ctor_literals[stmt.name] = literal_record
2341
2590
  # Mark input-backed iff the init references an input, so the reset
2342
2591
  # emits override-aware get_input_*() reads for it.
2343
- import re as _re
2344
- toks = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", expr_str))
2345
- if any(t in self._input_backed_vars for t in toks):
2592
+ if self._refs_input(expr_str):
2346
2593
  self._input_backed_vars.add(stmt.name)
2347
2594
 
2348
2595
  @staticmethod
@@ -2656,7 +2903,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2656
2903
  # Fold to a compile-time literal when possible (so the ctor-init
2657
2904
  # list can use it directly); otherwise record the raw expression
2658
2905
  # for the runtime reset path to expand.
2659
- folded = self._resolve_known(final)
2906
+ folded = self._resolve_known_through_inputs(final)
2660
2907
  if self._is_compile_time_value(folded):
2661
2908
  try:
2662
2909
  num = float(folded)
@@ -2669,9 +2916,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2669
2916
  self._stable_runtime_vars.add(target_name)
2670
2917
  # Mark input-backed iff the expression references an input so the
2671
2918
  # override-aware get_input_*() reads are emitted on the reset path.
2672
- import re as _re
2673
- toks = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", final))
2674
- if any(t in self._input_backed_vars for t in toks):
2919
+ if self._refs_input(final):
2675
2920
  self._input_backed_vars.add(target_name)
2676
2921
 
2677
2922
  def _find_reassigned_vars(self) -> set[str]:
@@ -2916,6 +3161,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2916
3161
  for a in node.args)
2917
3162
  if namespace == "input":
2918
3163
  return True
3164
+ # The generic ``input(...)`` is the leaf an inline TA length
3165
+ # already admits (``_is_stable_inline_input``: not the source
3166
+ # overload ``input(close)``, a constant defval); it used to fall to
3167
+ # the user-function branch below and read as a series.
3168
+ if namespace is None and func_name == "input":
3169
+ return self._is_stable_inline_input(node)
2919
3170
  if namespace is None and func_name in ("int", "float", "bool", "string"):
2920
3171
  return all(self._expr_is_stable(a, _udf_stack, _depth)
2921
3172
  for a in node.args)
@@ -2948,7 +3199,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2948
3199
  # tree: Pine grouping ``(a - b) / (c - d)`` degrades to ``a - b / c - d``
2949
3200
  # under C++ precedence. See ``_runtime_ctor_arg_for_reset`` (the string is
2950
3201
  # re-parsed and lowered through the expression visitor).
2951
- _ATOMIC_ARITH_NODES = (NumberLiteral, Identifier, MemberAccess, FuncCall)
3202
+ _ATOMIC_ARITH_NODES = (NumberLiteral, StringLiteral, Identifier, MemberAccess, FuncCall)
2952
3203
 
2953
3204
  def _arith_operand_to_str(self, node, _udf_stack: frozenset = frozenset(),
2954
3205
  _depth: int = 0) -> str | None:
@@ -2973,11 +3224,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2973
3224
  against recursion cycles (Pine forbids recursion, but a malformed source
2974
3225
  must be refused, not looped forever).
2975
3226
  """
3227
+ if getattr(self, "_arith_udf_memo", None) is None:
3228
+ self._arith_udf_memo = {}
3229
+ try:
3230
+ return self._arith_expr_to_str(node, _udf_stack, _depth)
3231
+ finally:
3232
+ self._arith_udf_memo = None
2976
3233
  if isinstance(node, NumberLiteral):
2977
3234
  v = node.value
2978
3235
  if isinstance(v, float) and v == int(v):
2979
3236
  return str(int(v))
2980
3237
  return str(v)
3238
+ if isinstance(node, StringLiteral):
3239
+ # ``len = mode == "Fast" ? 8 : 21``: a length chosen by comparing
3240
+ # an input.string with its options.
3241
+ return pine_string_literal(node.value)
2981
3242
  if isinstance(node, Identifier):
2982
3243
  return node.name
2983
3244
  if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
@@ -3009,12 +3270,27 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3009
3270
  # ordinary ``callee(args)`` rendering below. The stack/depth guard
3010
3271
  # refuses a recursive UDF (returns None -> caller leaves it untracked
3011
3272
  # -> the ctor guard rejects it loudly) instead of recursing forever.
3273
+ # An inline input call is spelled in full (title, keyword args):
3274
+ # the reset re-parses it and keys the override by that title. An
3275
+ # untitled call a declaration names carries that name as title=.
3276
+ if self._is_stable_inline_input(node):
3277
+ return spell_input_call(node, title=self._input_spelling_title(node))
3012
3278
  fn, ns = self._resolve_callee(node.callee)
3013
3279
  if ns is None and fn is not None and self._get_udf_def(fn) is not None:
3014
- inlined = self._inline_single_expr_udf(node, _udf_stack, _depth)
3015
- if inlined is None:
3016
- return None
3017
- return self._arith_expr_to_str(inlined, _udf_stack | {fn}, _depth + 1)
3280
+ # A call of the same function on the same argument nodes (a
3281
+ # caller's inlined body calling it twice, a diamond of such
3282
+ # helpers) is spelled once; the entry keeps the arguments
3283
+ # alive, so their ids stay theirs until the outermost call.
3284
+ key = (fn, _depth, _udf_stack, tuple(id(a) for a in node.args), bool(node.kwargs))
3285
+ memo = self._arith_udf_memo
3286
+ if key not in memo:
3287
+ inlined = self._inline_single_expr_udf(node, _udf_stack, _depth)
3288
+ memo[key] = (
3289
+ None if inlined is None
3290
+ else self._arith_expr_to_str(inlined, _udf_stack | {fn}, _depth + 1),
3291
+ node.args,
3292
+ )
3293
+ return memo[key][0]
3018
3294
  callee = self._arith_expr_to_str(node.callee, _udf_stack, _depth)
3019
3295
  if callee is None:
3020
3296
  return None
@@ -3027,14 +3303,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3027
3303
  return f"{callee}({', '.join(parts)})"
3028
3304
  return None
3029
3305
 
3306
+ def _is_var_input_binding(self, node: VarDecl) -> bool:
3307
+ """``var v = input.*()``: a ``var`` bound straight to an input call."""
3308
+ return (node.is_var and not node.is_varip
3309
+ and isinstance(node.value, FuncCall)
3310
+ and self._is_input_call(node.value))
3311
+
3030
3312
  def _collect_known_var(self, node: VarDecl) -> None:
3031
3313
  """Extract known constant value from a VarDecl."""
3032
3314
  # Don't inline series variables — their values change over time
3033
3315
  if self._decl_binding_is_series(id(node), node.name):
3034
3316
  return
3035
3317
  # Don't inline var/varip variables — they're mutable state that persists
3036
- # across bars and can be reassigned with :=
3037
- if node.is_var or node.is_varip:
3318
+ # across bars and can be reassigned with := (callers pass never-
3319
+ # reassigned names only, so ``var v = input.*()`` is the plain binding).
3320
+ if (node.is_var or node.is_varip) and not self._is_var_input_binding(node):
3038
3321
  return
3039
3322
  if isinstance(node.value, NumberLiteral):
3040
3323
  self._known_vars[node.name] = node.value.value
@@ -3049,6 +3332,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3049
3332
  self._input_backed_vars.add(node.name)
3050
3333
  if node.value.name in self._input_var_to_call:
3051
3334
  self._input_var_to_call[node.name] = self._input_var_to_call[node.value.name]
3335
+ # A plain alias of a derived stable scalar (``emaLen = calcEmaLen``
3336
+ # over ``calcEmaLen = swingLen * emaRatio``) holds that scalar on
3337
+ # every bar. Recorded as a derived expression of the name it
3338
+ # copies, the TA runtime reset re-expands it to the inputs'
3339
+ # getter reads; unrecorded, the reset spelled the alias's own
3340
+ # member, which the body has not assigned yet on the first bar
3341
+ # (0: ``ta.ema(close, emaLen)`` ran as an EMA of length 0).
3342
+ src = node.value.name
3343
+ if (src in self._derived_input_expr
3344
+ and src not in self._input_var_to_call
3345
+ and self._expr_is_stable(node.value)):
3346
+ self._derived_input_expr[node.name] = src
3347
+ self._stable_runtime_vars.add(node.name)
3348
+ if src in self._input_backed_vars:
3349
+ self._input_backed_vars.add(node.name)
3052
3350
  if node.value.name in self._timeframe_period_vars:
3053
3351
  self._timeframe_period_vars.add(node.name)
3054
3352
  elif (isinstance(node.value, MemberAccess)
@@ -3058,25 +3356,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3058
3356
  self._timeframe_period_vars.add(node.name)
3059
3357
  # Input calls: extract default value
3060
3358
  elif isinstance(node.value, FuncCall) and self._is_input_call(node.value):
3061
- default = self._get_input_default(node.value)
3062
- stored = False
3063
- if isinstance(default, NumberLiteral):
3064
- self._known_vars[node.name] = default.value
3065
- stored = True
3066
- elif isinstance(default, BoolLiteral):
3067
- self._known_vars[node.name] = default.value
3068
- stored = True
3069
- elif isinstance(default, StringLiteral):
3070
- self._known_vars[node.name] = default.value
3071
- stored = True
3072
- elif isinstance(default, MemberAccess) and isinstance(default.object, Identifier):
3073
- en = default.object.name
3074
- if en in self._enum_defs and default.member in self._enum_defs[en]:
3075
- self._known_vars[node.name] = self._enum_defs[en].index(
3076
- default.member
3077
- )
3078
- stored = True
3359
+ stored, default = self._input_default_value(node.value)
3079
3360
  if stored:
3361
+ self._known_vars[node.name] = default
3080
3362
  self._input_backed_vars.add(node.name)
3081
3363
  self._input_var_to_call[node.name] = node.value
3082
3364
  # Class-scope arithmetic / ternaries / casts over known, input-backed,
@@ -3099,8 +3381,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3099
3381
  expr_str = self._arith_expr_to_str(node.value)
3100
3382
  if expr_str is not None and self._expr_is_stable(node.value):
3101
3383
  import re as _re
3102
- tokens = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", expr_str))
3103
- refs_input = any(t in self._input_backed_vars for t in tokens)
3384
+ tokens = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*",
3385
+ blank_string_literals(
3386
+ self._inline_inputs_masked(expr_str))))
3387
+ refs_input = self._refs_input(expr_str)
3104
3388
  refs_derived = any(t in self._derived_input_expr for t in tokens)
3105
3389
  # The stability classifier already proved this expression is a
3106
3390
  # bar-invariant scalar (inputs / constants / timeframe.* /
@@ -3108,7 +3392,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3108
3392
  # stable exprs (and the TA reset path) can reference / expand
3109
3393
  # it — e.g. ``pi = math.asin(1) * 2`` feeds ``beta`` feeds
3110
3394
  # ``alpha`` feeds a function-local ``filterLen``.
3111
- folded = self._resolve_known(expr_str)
3395
+ folded = self._resolve_known_through_inputs(expr_str)
3112
3396
  if self._is_compile_time_value(folded):
3113
3397
  try:
3114
3398
  num = float(folded)
@@ -3134,25 +3418,38 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3134
3418
  # Public entry point
3135
3419
  # ------------------------------------------------------------------
3136
3420
 
3421
+ def _codegen_warning(self, node: ASTNode | None, message: str,
3422
+ hint: str | None = None) -> None:
3423
+ """Record a warning on ``ctx.diagnostics``, the list ``transpile_full``
3424
+ returns beside the C++ (after the support checker's)."""
3425
+ loc = node.loc if node is not None else None
3426
+ if loc is None:
3427
+ loc = SourceLocation(file=self.ctx.filename, line=1, col=1, end_col=1)
3428
+ self.ctx.diagnostics.append(Diagnostic(
3429
+ level=Level.WARNING, phase=Phase.CODEGEN, location=loc,
3430
+ message=message, hint=hint))
3431
+
3137
3432
  def _codegen_error(self, node: ASTNode | None, message: str, hint: str | None = None) -> None:
3433
+ raise CompileError([self._codegen_error_diagnostic(node, message, hint)])
3434
+
3435
+ def _codegen_error_diagnostic(self, node: ASTNode | None, message: str,
3436
+ hint: str | None = None) -> Diagnostic:
3138
3437
  loc = node.loc if node is not None else None
3139
3438
  if loc is None:
3140
3439
  loc = SourceLocation(file=self.ctx.filename, line=1, col=1, end_col=1)
3141
- raise CompileError(
3142
- [
3143
- Diagnostic(
3144
- level=Level.ERROR,
3145
- phase=Phase.CODEGEN,
3146
- location=loc,
3147
- message=message,
3148
- hint=hint,
3149
- )
3150
- ]
3440
+ return Diagnostic(
3441
+ level=Level.ERROR,
3442
+ phase=Phase.CODEGEN,
3443
+ location=loc,
3444
+ message=message,
3445
+ hint=hint,
3151
3446
  )
3152
3447
 
3153
3448
  def _ta_return_type(self, site: TACallSite) -> str:
3154
3449
  if getattr(site, "returns_tuple", False):
3155
- return f"{site.class_name}Result"
3450
+ return TA_TUPLE_RESULT_TYPES.get(
3451
+ self._ta_name_from_site(site), f"{site.class_name}Result"
3452
+ )
3156
3453
  if site.class_name in ("ta::Crossover", "ta::Crossunder", "ta::Cross"):
3157
3454
  return "bool"
3158
3455
  return "double"
@@ -3235,11 +3532,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3235
3532
  """
3236
3533
  self._inline_history_members = []
3237
3534
  self._inline_history_member_by_key = {}
3238
- counters = {
3239
- "hist_call": 0,
3240
- "series_arg": 0,
3241
- "udf_series_arg": 0,
3242
- }
3535
+ # session.* flags a function reads at an offset (owner -> flags) and
3536
+ # the owner of each such read: one Series per flag and emitted call
3537
+ # site, pushed at the function's entry (emit_top.py).
3538
+ self._session_call_flags: dict[str, set[str]] = {}
3539
+ self._session_call_owner: dict[int, str] = {}
3540
+ counters = {kind: 0 for kind in INLINE_HISTORY_KINDS}
3541
+ # A per-call session Series is numbered past a script name spelled
3542
+ # like one; the older kinds keep their numbering.
3543
+ authored_names = set(getattr(self, "_safe_name_occupied", ()))
3243
3544
 
3244
3545
  def walk_nodes(value):
3245
3546
  """Yield AST nodes in stable field order, including tuple elements.
@@ -3335,6 +3636,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3335
3636
  return
3336
3637
  counters[kind] += 1
3337
3638
  member_name = f"_{kind}_{counters[kind]}"
3639
+ while kind == "session_call" and member_name in authored_names:
3640
+ counters[kind] += 1
3641
+ member_name = f"_{kind}_{counters[kind]}"
3338
3642
  self._inline_history_member_by_key[key] = member_name
3339
3643
  self._inline_history_members.append({
3340
3644
  "kind": kind,
@@ -3381,6 +3685,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3381
3685
  return list(
3382
3686
  self._bind_typed_method_args(method_info, call).args_by_param
3383
3687
  )
3688
+ if isinstance(call.callee, Identifier):
3689
+ defaulted = self._user_call_args_with_defaults(
3690
+ call.callee.name, call
3691
+ )
3692
+ if defaulted is not None:
3693
+ return defaulted
3384
3694
  if call.kwargs:
3385
3695
  return _merge_kwargs(call.args, call.kwargs, params, lambda arg: arg)
3386
3696
  return list(call.args)
@@ -3436,6 +3746,20 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3436
3746
  id(child) for child in walk_nodes(expression)
3437
3747
  )
3438
3748
  self._requested_context_inline_node_ids = requested_node_ids
3749
+ # The session.* reads a function body emits, outside its own
3750
+ # request.security expressions (session_reads.py); a callable that
3751
+ # cannot keep its call sites apart, or that this analysis did not
3752
+ # clone, gets no per-call Series (a read it emits is refused, or asks
3753
+ # for the clones).
3754
+ no_call_history = (
3755
+ set(getattr(self.ctx, "session_history_unsafe", None) or {})
3756
+ | set(getattr(self.ctx, "session_uncloned", None) or ())
3757
+ )
3758
+ emitted_function_reads = {
3759
+ id(read)
3760
+ for fi in self.ctx.func_infos if fi.node is not None
3761
+ for read in emitted_session_reads(fi.node, requested_node_ids)
3762
+ }
3439
3763
 
3440
3764
  def emitted_context_for_call(
3441
3765
  fi,
@@ -3524,6 +3848,27 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3524
3848
  register(
3525
3849
  "hist_call", (id(node),), self._infer_type(node.object), owner
3526
3850
  )
3851
+ elif (isinstance(node, Subscript)
3852
+ and self._is_session_flag(node.object)):
3853
+ # In a function body the flag's history is the calls' of its
3854
+ # call site: one Series per flag and emitted variant (the
3855
+ # analyzer clones the function per call site), pushed once
3856
+ # per call at the function's entry. A top-level read uses the
3857
+ # flag's per-bar Series (_prescan_session_history), and one
3858
+ # written in a request.security expression the requested
3859
+ # clock's (security.py).
3860
+ if owner is not None and id(node) not in requested_node_ids:
3861
+ self._session_call_owner[id(node)] = owner
3862
+ if (id(node) in emitted_function_reads
3863
+ and owner not in no_call_history):
3864
+ flag = node.object.member
3865
+ self._session_call_flags.setdefault(owner, set()).add(flag)
3866
+ register("session_call", (owner, flag), "bool", owner)
3867
+ elif (isinstance(node, Subscript)
3868
+ and self._is_compound_history_object(node.object)):
3869
+ register(
3870
+ "hist_call", (id(node),), self._infer_type(node.object), owner
3871
+ )
3527
3872
 
3528
3873
  if not isinstance(node, FuncCall):
3529
3874
  continue
@@ -3644,12 +3989,155 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3644
3989
  context,
3645
3990
  )
3646
3991
 
3992
+ # A plain UDF reading a script variable (or bar_index) through history
3993
+ # reads its call site's history of it, on the same chart clock as a
3994
+ # history parameter: every chart-executed body owns one buffer per
3995
+ # variable, updated when the body is entered. A body that is emitted
3996
+ # once (never called) or only for a requested context keeps the
3997
+ # chart's history.
3998
+ global_reads = getattr(self.ctx, "func_global_history_reads", {}) or {}
3999
+ for fi in self.ctx.func_infos:
4000
+ names = global_reads.get(fi.name)
4001
+ if not names or fi.node is None:
4002
+ continue
4003
+ for context in self._inline_history_contexts_for_owner(fi.name):
4004
+ if (context is None or (fi.name, context)
4005
+ in self._requested_context_only_inline_contexts):
4006
+ continue
4007
+ for name in names:
4008
+ register_one(
4009
+ "fn_global_hist",
4010
+ (id(fi.node), name),
4011
+ self._series_type_for(name),
4012
+ context,
4013
+ )
4014
+
4015
+ def _function_global_history_member(self, name: str) -> str | None:
4016
+ """The emitted body's buffer of script variable ``name``, or None."""
4017
+ fi = self._func_info_map.get(getattr(self, "_active_func_name", None))
4018
+ if fi is None or fi.node is None or self._security_payload_depth:
4019
+ return None
4020
+ return self._inline_history_member_by_key.get(
4021
+ ("fn_global_hist", id(fi.node), name, self._current_instance_name)
4022
+ )
4023
+
4024
+ def _is_compound_history_object(self, node) -> bool:
4025
+ """Whether ``node[k]`` is history on an operator expression or a
4026
+ ``session.*`` flag.
4027
+
4028
+ ``(a > b)[1]``, ``(x - y)[2]`` and ``(c ? p : q)[1]`` read the
4029
+ expression's value k bars ago; its C++ scalar cannot be indexed, so
4030
+ the subscript owns a synthetic ``_hist_call_*`` Series exactly like an
4031
+ inline call result. Numeric and bool expressions only: other families
4032
+ keep their established lowering. A ``session.*`` flag's C++ value
4033
+ cannot be indexed either: a request.security payload keeps its history
4034
+ on the requested clock like an operator expression's (security.py); a
4035
+ function body reads its call site's per-call Series
4036
+ (``_prepare_inline_history_members``) and a top-level read the flag's
4037
+ per-bar Series (``_prescan_session_history``).
4038
+ """
4039
+ if self._is_session_flag(node):
4040
+ return True
4041
+ return (isinstance(node, (BinOp, UnaryOp, Ternary))
4042
+ and self._infer_type(node) in ("double", "int", "int64_t", "bool"))
4043
+
4044
+ @staticmethod
4045
+ def _is_session_flag(node) -> bool:
4046
+ """Whether ``node`` is a ``session.*`` boolean (``session.ismarket``)."""
4047
+ return (isinstance(node, MemberAccess)
4048
+ and isinstance(node.object, Identifier)
4049
+ and node.object.name == "session"
4050
+ and node.member in SESSION_FLAG_MEMBERS)
4051
+
4052
+ def _session_history_member(self, flag: str) -> str:
4053
+ """The per-bar Series of a ``session.*`` flag read at the top level:
4054
+ ``_pf_session_hist_<flag>``, suffixed past a script name spelled so."""
4055
+ return self._session_history_member_names[flag]
4056
+
4057
+ def _settle_session_reads(self, cpp: str) -> None:
4058
+ """Refuse the ``session.<flag>[k]`` reads the C++ emits with no
4059
+ history, or, for a function this analysis did not clone
4060
+ (``session_uncloned``), ask for its clones
4061
+ (``session_functions_needing_clones``).
4062
+
4063
+ Both follow the emitted code, not the rendered reads: a call can render
4064
+ an argument and leave it out of the C++ (``color.from_gradient``'s
4065
+ arguments, a drawing's xloc), and a read there refuses nothing and
4066
+ needs no clone. Such a read renders as a stand-in name
4067
+ (``_refused_session_read``), which only that read can spell in code.
4068
+ """
4069
+ if not self._refused_session_reads:
4070
+ return
4071
+ pattern = re.compile(r"(?<![\w.])(" + "|".join(map(
4072
+ re.escape, sorted(self._refused_session_reads, key=len, reverse=True))) + r")\b")
4073
+ emitted = set(pattern.findall(cpp_code_only(cpp)))
4074
+ # One error per read, the refusal rendered first (a read can be
4075
+ # rendered on the chart and in a request.security evaluator).
4076
+ refused: dict[tuple[int, int], Diagnostic] = {}
4077
+ needing: set[str] = set()
4078
+ for name, (_read, where, message, uncloned) in self._refused_session_reads.items():
4079
+ if name not in emitted:
4080
+ continue
4081
+ if uncloned is not None:
4082
+ needing.add(uncloned)
4083
+ continue
4084
+ diagnostic = self._codegen_error_diagnostic(where, message)
4085
+ refused.setdefault(
4086
+ (diagnostic.location.line, diagnostic.location.col), diagnostic)
4087
+ if refused:
4088
+ raise CompileError([refused[key] for key in sorted(refused)])
4089
+ self.session_functions_needing_clones = frozenset(needing)
4090
+
4091
+ def _prescan_session_history(self) -> None:
4092
+ """The ``session.*`` flags the script reads at an offset at its top
4093
+ level, each of which gets one Series pushed on every chart bar.
4094
+
4095
+ TradingView reads a flag's history by bars at the top level of the
4096
+ script, in a block and on a lazy operand as well, and by calls inside a
4097
+ function (``tests/test_e2e_session_history.py``): function bodies
4098
+ (``_prepare_inline_history_members``) and ``request.security`` payloads
4099
+ (the requested clock, security.py) keep their own history instead. A
4100
+ ``// @pf-trace`` expression is read at the top level.
4101
+ """
4102
+ roots = [stmt for stmt in self.ctx.ast.body
4103
+ if not isinstance(stmt, (FuncDef, MethodDef))]
4104
+ roots += [pragma.expr_node for pragma in (self.ctx.pf_trace_pragmas or [])
4105
+ if getattr(pragma, "expr_node", None) is not None]
4106
+ payloads: set[int] = set()
4107
+ for root in roots:
4108
+ for node, _depth in iter_ast_nodes(root):
4109
+ if not isinstance(node, FuncCall):
4110
+ continue
4111
+ name, namespace = self._resolve_callee(node.callee)
4112
+ if namespace == "request" and name in ("security", "security_lower_tf"):
4113
+ payload = node.args[2] if len(node.args) > 2 else node.kwargs.get("expression")
4114
+ if payload is not None:
4115
+ payloads.add(id(payload))
4116
+ flags = {read.object.member for root in roots
4117
+ for read in emitted_session_reads(root, payloads)}
4118
+ used = self._session_names_used
4119
+ self._session_history_member_names = {
4120
+ flag: self._allocate_generated_cpp_name(f"_pf_session_hist_{flag}", used)
4121
+ for flag in sorted(flags)
4122
+ }
4123
+ self._session_history_flags = flags
4124
+
3647
4125
  def _inline_history_member(self, kind: str, node: ASTNode,
3648
4126
  arg_idx: int | None = None) -> str:
3649
4127
  source_key = (id(node),) if arg_idx is None else (id(node), arg_idx)
3650
4128
  key = (kind, *source_key, self._current_instance_name)
3651
4129
  member = self._inline_history_member_by_key.get(key)
3652
4130
  if member is None:
4131
+ if getattr(self, "_security_fallback_frame", None) is not None:
4132
+ # A builtin call in a request.security payload reads history
4133
+ # the evaluator keeps no series for (a helper call's inside a
4134
+ # helper body the payload inlines twice): refused as the bare
4135
+ # read is, where this used to crash the transpiler.
4136
+ self._codegen_error(
4137
+ node,
4138
+ "request.security helper call history is only supported in "
4139
+ "the payload itself",
4140
+ )
3653
4141
  raise AssertionError(
3654
4142
  "missing pre-registered inline history member for "
3655
4143
  f"{kind} at {getattr(node, 'loc', None)} in context "
@@ -3791,8 +4279,47 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3791
4279
  self._udt_arena_member_names[type_name] = allocated
3792
4280
  self._all_member_names.add(allocated)
3793
4281
 
4282
+ # C++ types one member holds for every declaration of a name.
4283
+ _SHARED_MEMBER_TYPES = frozenset({"int", "int64_t", "double", "bool"})
4284
+
4285
+ def _block_locals_needing_names(self) -> frozenset:
4286
+ """The declarations in the script's top-level blocks whose type the
4287
+ member of their name cannot hold (``block_locals``). The member has
4288
+ the type of the name's direct top-level declaration, else of its
4289
+ first declaration; a string, a collection or an object beside another
4290
+ type did not compile. Numbers and bools share one member, as before."""
4291
+ decls = [(decl, nested) for decl, nested in block_declarations(self.ctx.ast.body)
4292
+ if not decl.is_var and not decl.is_varip and decl.value is not None]
4293
+ member: dict[str, str | None] = {}
4294
+ for decl, nested in decls:
4295
+ if not nested:
4296
+ member.setdefault(decl.name, self._declared_cpp_type(decl))
4297
+ needing: set = set()
4298
+ for decl, nested in decls:
4299
+ cpp = self._declared_cpp_type(decl)
4300
+ first = member.setdefault(decl.name, cpp)
4301
+ if (nested and first is not None and cpp is not None and first != cpp
4302
+ and not {first, cpp} <= self._SHARED_MEMBER_TYPES
4303
+ and decl_key(decl) is not None):
4304
+ needing.add(decl_key(decl))
4305
+ return frozenset(needing)
4306
+
4307
+ def _declared_cpp_type(self, decl: VarDecl) -> str | None:
4308
+ """The C++ type of a declaration's value (its hint's, when typed)."""
4309
+ if decl.type_hint:
4310
+ spec = self._type_spec_from_hint_name(decl.type_hint)
4311
+ return self._type_spec_to_cpp(spec) if spec is not None else None
4312
+ try:
4313
+ return self._infer_type(decl.value)
4314
+ except Exception:
4315
+ return None
4316
+
3794
4317
  def generate(self) -> str:
3795
4318
  """Generate C++ source from the AnalyzerContext."""
4319
+ # Every input is keyed by its title: refuse a non-constant one first,
4320
+ # then flag inputs one override key would reach together.
4321
+ self._check_input_titles()
4322
+ self._check_input_keys()
3796
4323
  # Context-sensitive instance pre-pass (needs the naming helpers populated
3797
4324
  # in __init__). Computes nested stateful-helper dispatch + fresh instances.
3798
4325
  self._build_func_instances()
@@ -3806,14 +4333,26 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3806
4333
  self._prepare_udt_generated_names()
3807
4334
  # Pre-scan for strategy series vars
3808
4335
  self._prescan_strategy_series()
4336
+ self._prescan_session_history()
3809
4337
  self._security_ohlc_hist_fields_by_sec: dict[int, set[str]] = {}
4338
+ # A source input's selected series read at a history offset in a
4339
+ # payload: (key, default) -> (the input call, its history field).
4340
+ self._security_source_hist_fields: dict[tuple[str, str], tuple] = {}
3810
4341
  # request.security TA call-sites read at a history offset (``ta.ema(...)[k>=1]``).
3811
4342
  # Maps sec_id -> set of TA call-site indices needing an HTF history Series.
3812
4343
  self._security_ta_hist_idx_by_sec: dict[int, set[int]] = {}
3813
4344
  # request.security helper-call results read at a history offset
3814
4345
  # (``myHelper()[k]``). Maps (sec_id, node-id) -> backing Series metadata.
3815
4346
  self._security_expr_hist_by_node: dict[tuple[int, int], dict] = {}
4347
+ # request.security id -> the globals its payload reads with history
4348
+ # (``_security_global_history_value``).
4349
+ self._security_global_hist_names: dict[int, set[str]] = {}
4350
+ # ids of the global history reads kept in a double (``_security_emits_double``).
4351
+ self._security_double_hist_nodes: set[int] = set()
4352
+ # request.security ids whose payload reads the requested bar_index.
4353
+ self._security_bar_index_secs: set[int] = set()
3816
4354
  self._prepare_lazy_source_clock_sites()
4355
+ self.block_locals_needing_names = self._block_locals_needing_names()
3817
4356
 
3818
4357
  lines: list[str] = []
3819
4358
 
@@ -3835,11 +4374,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3835
4374
  # The two-phase order also permits self/nested UDT fields without
3836
4375
  # embedding a C++ type recursively by value.
3837
4376
  for type_name in self._udt_defs:
3838
- lines.append(f"struct {type_name} {{")
4377
+ safe_type_name = self._safe_name(type_name)
4378
+ lines.append(f"struct {safe_type_name} {{")
3839
4379
  lines.append(" int32_t __pf_id = -1;")
3840
4380
  lines.append("};")
3841
4381
  lines.append(
3842
- f"inline bool is_na(const {type_name}& _z) "
4382
+ f"inline bool is_na(const {safe_type_name}& _z) "
3843
4383
  "{ return _z.__pf_id < 0; }"
3844
4384
  )
3845
4385
  lines.append("")
@@ -3865,6 +4405,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3865
4405
  f" {self._udt_undo_coordinator_cpp_name}& operator=(",
3866
4406
  f" {self._udt_undo_coordinator_cpp_name}&&) = delete;",
3867
4407
  "",
4408
+ " void reset_for_run() {",
4409
+ " _pf_undo_.clear();",
4410
+ " _pf_active_ = false;",
4411
+ " // Keep generation monotonic: an old token cannot become valid again.",
4412
+ " }",
4413
+ "",
3868
4414
  " Snapshot snapshot() {",
3869
4415
  " if (_pf_generation_ == std::numeric_limits<uint64_t>::max()) {",
3870
4416
  ' throw std::overflow_error("UDT checkpoint generation exhausted");',
@@ -3942,6 +4488,14 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3942
4488
  f" {self._udt_arena_template_cpp_name}& operator=(",
3943
4489
  f" {self._udt_arena_template_cpp_name}&&) = delete;",
3944
4490
  "",
4491
+ " void reset_for_run() {",
4492
+ " _pf_records_.clear();",
4493
+ " _pf_checkpoint_size_ = 0;",
4494
+ " _pf_checkpoint_generation_ = 0;",
4495
+ " _pf_checkpoint_active_ = false;",
4496
+ " // The arena remains attached to its original coordinator.",
4497
+ " }",
4498
+ "",
3945
4499
  " _PFHandle create(_PFRecord value) {",
3946
4500
  " if (_pf_records_.size() > static_cast<std::size_t>(",
3947
4501
  " std::numeric_limits<int32_t>::max())) {",
@@ -4012,6 +4566,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4012
4566
  # the same map. Authored field defaults are evaluated by ``Type.new``
4013
4567
  # at the call site; record declarations use only inert typed defaults so
4014
4568
  # bar/TA expressions never leak into namespace-scope C++ initializers.
4569
+ if self._udt_has_array_fields():
4570
+ lines.append(UDT_ARRAY_FIELD_CPP)
4015
4571
  for type_name, fields in self._udt_defs.items():
4016
4572
  record_type = self._udt_record_cpp_type(type_name)
4017
4573
  lines.append(f"struct {record_type} {{")
@@ -4021,11 +4577,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4021
4577
  if f.name in omitted:
4022
4578
  continue
4023
4579
  spec = field_specs.get(f.name) or self._type_spec_from_hint_name(f.type_name)
4580
+ array_field = self._udt_array_field_cpp(spec, type_name)
4581
+ if array_field is not None:
4582
+ lines.append(f" {array_field} {self._safe_name(f.name)} = {array_field}();")
4583
+ continue
4024
4584
  cpp_type = self._type_spec_to_cpp(spec)
4025
4585
  if cpp_type == "int":
4026
4586
  cpp_type = "int64_t"
4027
4587
  default = self._default_for_spec(spec)
4028
- lines.append(f" {cpp_type} {f.name} = {default};")
4588
+ lines.append(f" {cpp_type} {self._safe_name(f.name)} = {default};")
4029
4589
  lines.append("};")
4030
4590
  lines.append("")
4031
4591
 
@@ -4034,15 +4594,18 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4034
4594
 
4035
4595
  # 1c. Enum constants + string tables for str.tostring(enumVar)
4036
4596
  for enum_name, members in self._enum_defs.items():
4597
+ safe_enum_name = self._safe_name(enum_name)
4037
4598
  for i, member in enumerate(members):
4038
- lines.append(f'const int {enum_name}_{member} = {i};')
4599
+ lines.append(
4600
+ f'const int {safe_enum_name}_{self._safe_name(member)} = {i};'
4601
+ )
4039
4602
  strs = self._enum_member_strings.get(enum_name)
4040
4603
  if strs and len(strs) == len(members):
4041
4604
  parts = ", ".join(
4042
4605
  f'std::string("{self._cpp_string_escape(s)}")' for s in strs
4043
4606
  )
4044
4607
  lines.append(
4045
- f"static const std::string {enum_name}_str_values[] = {{{parts}}};"
4608
+ f"static const std::string {safe_enum_name}_str_values[] = {{{parts}}};"
4046
4609
  )
4047
4610
  lines.append("")
4048
4611
 
@@ -4051,9 +4614,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4051
4614
  # per-callsite instances are declared below inside GeneratedStrategy
4052
4615
  # and therefore join the automatic COOF checkpoint inventory.
4053
4616
  self._emit_lazy_source_clock_helper(lines)
4617
+ # The chart-bar session type, inserted here once the class is lowered
4618
+ # and known to call it (codegen/session_market.py).
4619
+ _session_market_at = len(lines)
4054
4620
 
4055
4621
  # 2. Open class
4056
- lines.append("class GeneratedStrategy : public BacktestEngine {")
4622
+ lines.append("class GeneratedStrategy : public pineforge::source::PineStrategyHost {")
4057
4623
  lines.append("public:")
4058
4624
  _script_state_decl_start = len(lines)
4059
4625
 
@@ -4127,6 +4693,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4127
4693
  self._security_ohlc_hist_fields_by_sec[sec_id] = hist_fields
4128
4694
  for i, el in enumerate(expr_node.elements):
4129
4695
  ctype = self._infer_cpp_type_for_security_elem(el)
4696
+ if ctype == "int" and item.get("foreign"):
4697
+ # Another symbol's int element (its time, time_close,
4698
+ # bar_index) is held as a double: a millisecond
4699
+ # stamp overflows an int, and it reads na until the
4700
+ # symbol's first bar.
4701
+ ctype = "double"
4130
4702
  if ctype == "std::vector<double>":
4131
4703
  lines.append(f" {ctype} _req_sec_{sec_id}_{i}{{}};")
4132
4704
  else:
@@ -4162,7 +4734,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4162
4734
  self._security_ohlc_hist_fields_by_sec[sec_id] = (
4163
4735
  self._collect_security_ohlc_hist_fields_for_call(item)
4164
4736
  )
4165
- lines.append(f" double _req_sec_{sec_id} = na<double>();")
4737
+ if item.get("string_result"):
4738
+ lines.append(f" std::string _req_sec_{sec_id} = na<std::string>();")
4739
+ else:
4740
+ lines.append(f" double _req_sec_{sec_id} = na<double>();")
4166
4741
  for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
4167
4742
  ctype = self._security_bar_hist_type(field)
4168
4743
  lines.append(
@@ -4174,9 +4749,22 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4174
4749
  for name in self._security_ta_hist_series_names(sec_id):
4175
4750
  lines.append(f" Series<double> {name}{_mbb};")
4176
4751
  self._emit_security_expr_hist_members(sec_id, expr_node, lines, _mbb)
4177
-
4752
+ if item.get("foreign"):
4753
+ # configure_security_evaluators() clears it when the site is
4754
+ # registered: its reads stop the run while it is set.
4755
+ lines.append(f" bool _pf_sec_missing_{sec_id} = true;")
4756
+
4757
+ # A recorded request's missing-data flag, set where it is evaluated.
4758
+ for n in range(len(self._recorded_sites())):
4759
+ lines.append(f" bool _pf_rec_missing_{n} = true;")
4178
4760
  if self._security_calls:
4179
4761
  lines.append(' std::unordered_map<std::string, Series<double>> _security_helper_series_;')
4762
+ self._security_string_series_declared = self._security_needs_string_series()
4763
+ if self._security_string_series_declared:
4764
+ lines.append(
4765
+ ' std::unordered_map<std::string, Series<std::string>> '
4766
+ '_security_helper_series_str_;'
4767
+ )
4180
4768
 
4181
4769
  # Security-local mutable global state for request.security
4182
4770
  for info in self._security_eval_info:
@@ -4200,7 +4788,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4200
4788
  for _ta_idx, site in enumerate(self.ctx.ta_call_sites):
4201
4789
  if _ta_idx in self._dead_ta_indices:
4202
4790
  continue
4203
- lines.append(f" {site.class_name} {site.member_name};")
4791
+ lines.append(f" {self._ta_member_cpp_type(site)} {site.member_name};")
4204
4792
  if self._ta_site_uses_precalc(site):
4205
4793
  vtype = self._ta_return_type(site)
4206
4794
  lines.append(f" std::vector<{vtype}> _precalc_{site.member_name};")
@@ -4219,8 +4807,6 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4219
4807
  literal = info["length_literal"]
4220
4808
  capacity = f"{{{literal + 1}}}" if literal is not None and literal >= 1 else ""
4221
4809
  lines.append(f" Series<double> {info['hist']}{capacity};")
4222
- if info["chart"] is not None:
4223
- lines.append(f" Series<double> {info['chart']}{capacity};")
4224
4810
 
4225
4811
  # Security evaluator TA members (cloned from expression dependencies)
4226
4812
  # Skip for user function call expressions — their TA deps are internal to the function
@@ -4228,18 +4814,26 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4228
4814
  for idx, variants in (info.get("ta_variants") or {}).items():
4229
4815
  site = self.ctx.ta_call_sites[idx]
4230
4816
  for variant in variants:
4231
- lines.append(f" {site.class_name} {variant['member_name']};")
4817
+ cpp_type = self._ta_security_member_cpp_type(info["sec_id"], site, variant)
4818
+ lines.append(f" {cpp_type} {variant['member_name']};")
4232
4819
 
4233
4820
  # 4. Series members for bar field history
4234
4821
  for field_name in sorted(self.ctx.series_bar_fields):
4235
4822
  lines.append(f" Series<double> _s_{field_name}{_mbb};")
4236
4823
 
4237
4824
  # 5. var/varip members (deduplicate by name)
4825
+ # Sections 5-6 and 8c-8c2 declare the script's var/varip and
4826
+ # history-read variables, whose drawing handles keep a drawing from
4827
+ # the collection; 8b's plain non-var globals do not.
4828
+ _variable_decls_start = len(lines)
4238
4829
  seen_var_members: set[str] = set()
4239
4830
  for name, ptype, init_str in self.ctx.var_members:
4240
4831
  if name in seen_var_members:
4241
4832
  continue
4242
4833
  seen_var_members.add(name)
4834
+ if self._budget is not None:
4835
+ # _callable_var_udt_spec scans every declaration.
4836
+ self._budget.check(phase=Phase.CODEGEN)
4243
4837
  safe = self._safe_name(name)
4244
4838
  callable_collection_spec = (
4245
4839
  None
@@ -4347,7 +4941,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4347
4941
  udt_type = udt_name
4348
4942
  break
4349
4943
  if udt_type:
4350
- lines.append(f" {udt_type} {safe};")
4944
+ lines.append(f" {self._safe_name(udt_type)} {safe};")
4351
4945
  continue
4352
4946
  cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
4353
4947
  # Promote int->int64_t when init RHS is an int64-returning builtin
@@ -4387,6 +4981,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4387
4981
  cpp_type = self._series_type_for(name)
4388
4982
  lines.append(f" Series<{cpp_type}> {safe}{_mbb};")
4389
4983
 
4984
+ _variable_decls = lines[_variable_decls_start:]
4390
4985
  # 7. Fixnan members
4391
4986
  for _fi_idx, site in enumerate(self.ctx.fixnan_sites):
4392
4987
  if _fi_idx in self._dead_fixnan_indices:
@@ -4404,6 +4999,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4404
4999
  else:
4405
5000
  lines.append(f" Series<double> {svar}{_mbb};")
4406
5001
 
5002
+ # 8. session.* flags read at an offset at the top level: one Series per
5003
+ # flag, pushed on every chart bar (emit_top.py).
5004
+ for flag in sorted(self._session_history_flags):
5005
+ lines.append(f" Series<bool> {self._session_history_member(flag)}{_mbb};")
5006
+
4407
5007
  # 8a. Synthetic temporary history. Unlike the legacy function-local
4408
5008
  # static buffers, these members are value-copyable rollback state and
4409
5009
  # have one identity per source site / emitted UDF variant.
@@ -4450,10 +5050,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4450
5050
  if _draw_cpp is not None:
4451
5051
  lines.append(f" {_draw_cpp} {safe} = {_draw_cpp}{{}};")
4452
5052
  else:
4453
- lines.append(f" {udt_t} {safe} = {udt_t}{{}};")
5053
+ udt_cpp = self._safe_name(udt_t)
5054
+ lines.append(f" {udt_cpp} {safe} = {udt_cpp}{{}};")
4454
5055
  else:
4455
5056
  expr = self.ctx.global_expr_map.get(name) if hasattr(self.ctx, "global_expr_map") else None
4456
- if (
5057
+ if self._global_color_hint(name):
5058
+ cpp_type = "int64_t"
5059
+ elif self._global_bool_hint(name):
5060
+ cpp_type = "bool"
5061
+ elif (
4457
5062
  name in self._direct_program_tuple_binding_names
4458
5063
  and ptype == PineType.BOOL
4459
5064
  ):
@@ -4462,15 +5067,39 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4462
5067
  # coarse request.security call expression itself still
4463
5068
  # reports FLOAT, so prefer that exact binding type here.
4464
5069
  cpp_type = "bool"
5070
+ elif (
5071
+ ptype == PineType.STRING
5072
+ and name in self._security_tuple_binding_names()
5073
+ ):
5074
+ # A string element of a request.security helper tuple.
5075
+ cpp_type = "std::string"
5076
+ elif (
5077
+ ptype == PineType.STRING
5078
+ and isinstance(expr, (IfStmt, SwitchStmt))
5079
+ ):
5080
+ # A string element of an if/switch selection tuple
5081
+ # (``[a, b] = if c ... f() else [string(na), string(na)]``):
5082
+ # the whole selection infers as double, which a
5083
+ # std::string element could not be assigned to.
5084
+ cpp_type = "std::string"
4465
5085
  else:
4466
5086
  cpp_type = (
4467
5087
  self._infer_type(expr)
4468
5088
  if expr is not None
4469
5089
  else PINE_TYPE_TO_CPP.get(ptype, "double")
4470
5090
  )
5091
+ # The var-member rule: a plain global an epoch reaches
5092
+ # (``_wide_int_provenance``) is stored in 64 bits too.
5093
+ if cpp_type == "int" and self._is_int64_builtin_init(name):
5094
+ cpp_type = "int64_t"
5095
+ # An int that can hold TradingView's infinity
5096
+ # (``_nonfinite_int_names``) keeps its double.
5097
+ elif cpp_type == "int" and name in self._nonfinite_int_names():
5098
+ cpp_type = "double"
4471
5099
  default = self._default_for_type(cpp_type)
4472
5100
  lines.append(f" {cpp_type} {safe} = {default};")
4473
5101
 
5102
+ _variable_decls_start = len(lines)
4474
5103
  # 8c. Cloned var/series members for per-call-site function variants
4475
5104
  # Same pattern as TA member cloning: each call site gets its own copy
4476
5105
  emitted_clones: set[str] = set()
@@ -4496,6 +5125,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4496
5125
  orig_safe, fresh_safe, _mbb, lines, owner_func=owner_func
4497
5126
  )
4498
5127
 
5128
+ _variable_decls = _variable_decls + lines[_variable_decls_start:]
5129
+ self._drawing_pins = self._drawing_pin_members(_variable_decls)
5130
+
4499
5131
  # 8c3. Fresh fixnan members for context-sensitive helper instances.
4500
5132
  # Each fresh instance gets its OWN previous-value member so two
4501
5133
  # call paths never share fixnan state (mirrors 8c2 for vars).
@@ -4508,14 +5140,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4508
5140
 
4509
5141
  # 8d. Drawing-objects-as-data arenas (gated on _uses_drawing so
4510
5142
  # non-drawing strategies emit byte-identical C++). Each arena is a
4511
- # per-strategy member -> reset-per-run is automatic. Caps come from
4512
- # the strategy() header max_*_count (default 50; linefill default 50).
5143
+ # per-strategy member, reset by prepare_script_run. The arenas never
5144
+ # evict: the generated collectors apply the strategy() header's
5145
+ # max_*_count the way TradingView does (default 50; see
5146
+ # DRAWING_LIFETIME_CPP), and linefills are never collected.
4513
5147
  if self._uses_drawing:
4514
- caps = self._drawing_caps or {}
4515
- lines.append(f" DrawingArena<LineRec> _pf_lines_{{{caps.get('line', 50)}}};")
4516
- lines.append(f" DrawingArena<BoxRec> _pf_boxes_{{{caps.get('box', 50)}}};")
4517
- lines.append(f" DrawingArena<LabelRec> _pf_labels_{{{caps.get('label', 50)}}};")
4518
- lines.append(f" DrawingArena<LinefillRec> _pf_linefills_{{{caps.get('linefill', 50)}}};")
5148
+ lines.append(" DrawingArena<LineRec> _pf_lines_{_PF_DRAWING_UNBOUNDED};")
5149
+ lines.append(" DrawingArena<BoxRec> _pf_boxes_{_PF_DRAWING_UNBOUNDED};")
5150
+ lines.append(" DrawingArena<LabelRec> _pf_labels_{_PF_DRAWING_UNBOUNDED};")
5151
+ lines.append(" DrawingArena<LinefillRec> _pf_linefills_{_PF_DRAWING_UNBOUNDED};")
4519
5152
 
4520
5153
  # 9. _var_initialized flag
4521
5154
  if self.ctx.var_members:
@@ -4577,15 +5210,23 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4577
5210
  # inventory from the declarations above so every future generated
4578
5211
  # state category is captured automatically (or generation fails loudly
4579
5212
  # if it introduces an unfamiliar declaration form).
5213
+ _script_state_declarations = lines[_script_state_decl_start:-1]
4580
5214
  _script_state_members = self._collect_script_state_members(
4581
- lines[_script_state_decl_start:-1]
5215
+ _script_state_declarations
4582
5216
  )
4583
5217
  self._emit_script_state_hooks(lines, _script_state_members)
4584
5218
  lines.append("")
5219
+ # Its per-strategy cache, outside the checkpointed script state.
5220
+ _session_market_member_at = len(lines)
4585
5221
 
4586
5222
  # 9. Constructor with TA initializer list
4587
5223
  self._emit_constructor(lines)
4588
5224
  lines.append("")
5225
+ self._emit_script_run_prepare(lines, _script_state_declarations)
5226
+ lines.append("")
5227
+ if self._uses_drawing:
5228
+ self._emit_drawing_collectors(lines)
5229
+ lines.append("")
4589
5230
 
4590
5231
  # 10. User-defined functions (with per-call-site variants for functions
4591
5232
  # containing TA calls OR series variables that need isolation)
@@ -4647,7 +5288,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4647
5288
  # 13. extern "C" interface
4648
5289
  self._emit_extern_c(lines)
4649
5290
 
4650
- return "\n".join(lines)
5291
+ if self._uses_session_market:
5292
+ lines.insert(_session_market_member_at, SESSION_MARKET_MEMBER)
5293
+ lines.insert(_session_market_at, SESSION_MARKET_CPP)
5294
+
5295
+ cpp = "\n".join(lines)
5296
+ self._settle_session_reads(cpp)
5297
+ return cpp
4651
5298
 
4652
5299
  # ------------------------------------------------------------------
4653
5300
  # Top-level emitters (_emit_includes / _emit_constructor / _emit_on_bar
@@ -4696,8 +5343,99 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4696
5343
  and not self._known_var_is_lexically_shadowed(arg_str)
4697
5344
  ):
4698
5345
  return str(self._stable_var_ctor_literals[arg_str].value)
5346
+ # An inline input's placeholder is its defval, as a bound
5347
+ # ``v = input.int(9, ...)`` contributes through ``_known_vars``; the
5348
+ # runtime reset still sizes the indicator from the live input.
5349
+ folded = self._fold_inline_input_defaults(arg_str)
5350
+ if folded is not None and self._is_compile_time_value(folded):
5351
+ return folded
4699
5352
  return resolved
4700
5353
 
5354
+ @staticmethod
5355
+ def _ta_ctor_arg_is_bool(site: TACallSite, position: int) -> bool:
5356
+ """Whether a TA shim constructor slot is a Pine simple bool."""
5357
+ return (
5358
+ (site.class_name == "_PFALMA" and position == 3)
5359
+ or (site.class_name in ("_PFKC", "_PFKCW") and position == 2)
5360
+ or (site.class_name == "ta::TR" and position == 0)
5361
+ or (site.class_name in ("ta::StdDev", "ta::Variance") and position == 1)
5362
+ )
5363
+
5364
+ @staticmethod
5365
+ def _ta_ctor_bool_cpp(value: str) -> str:
5366
+ """Pine truthiness for a runtime TA constructor flag."""
5367
+ if value.strip() in {"true", "false"}:
5368
+ return value
5369
+ return pine_truth_cast(value)
5370
+
5371
+ # An inline ``input.*()`` call inside a ctor-arg / derived-length spelling
5372
+ # is one leaf of the expression. It qualifies exactly when its bound
5373
+ # spelling ``v = <call>`` would make ``v`` input-backed
5374
+ # (``_collect_known_var``): not a source input, and a constant defval.
5375
+
5376
+ def _is_stable_inline_input(self, node) -> bool:
5377
+ return (isinstance(node, FuncCall)
5378
+ and self._is_input_call(node)
5379
+ and not self._is_source_input(node)
5380
+ and self._input_default_value(node)[0])
5381
+
5382
+ def _inline_input_calls(self, expr: str) -> list[tuple[int, int, FuncCall]] | None:
5383
+ """Each inline input call in ``expr`` as ``(start, end, call)``; None
5384
+ when one is not a stable inline input or does not re-parse."""
5385
+ from ..lexer import Lexer
5386
+ from ..parser import Parser
5387
+ calls = []
5388
+ for start, end in input_call_spans(expr):
5389
+ text = expr[start:end]
5390
+ try:
5391
+ node = Parser(Lexer(text).tokenize(), source=text)._parse_expression()
5392
+ except Exception:
5393
+ return None
5394
+ if not self._is_stable_inline_input(node):
5395
+ return None
5396
+ calls.append((start, end, node))
5397
+ return calls
5398
+
5399
+ def _inline_inputs_masked(self, expr: str) -> str:
5400
+ """``expr`` with each inline input call reduced to the bare word
5401
+ ``input``, so identifier scans never read its title string or keyword
5402
+ names. Unchanged when one of the calls is not a stable inline input."""
5403
+ for start, end, _call in reversed(self._inline_input_calls(expr) or ()):
5404
+ expr = expr[:start] + "input" + expr[end:]
5405
+ return expr
5406
+
5407
+ def _refs_input(self, expr: str) -> bool:
5408
+ """Whether a stable-expression spelling reads an input: an inline
5409
+ input call or an input-backed name."""
5410
+ masked = self._inline_inputs_masked(expr)
5411
+ if masked != expr:
5412
+ return True
5413
+ return any(t in self._input_backed_vars
5414
+ for t in re.findall(r"[A-Za-z_][A-Za-z_0-9]*",
5415
+ blank_string_literals(expr)))
5416
+
5417
+ def _fold_inline_input_defaults(self, expr: str) -> str | None:
5418
+ """``expr`` const-folded with each inline input call read as its
5419
+ defval; None when it has none or one is not a stable inline input."""
5420
+ calls = self._inline_input_calls(expr)
5421
+ if not calls:
5422
+ return None
5423
+ for start, end, call in reversed(calls):
5424
+ value = self._input_default_value(call)[1]
5425
+ if isinstance(value, bool):
5426
+ literal = "true" if value else "false"
5427
+ elif isinstance(value, str):
5428
+ literal = pine_string_literal(value)
5429
+ else:
5430
+ literal = str(value)
5431
+ expr = expr[:start] + literal + expr[end:]
5432
+ return self._resolve_known(expr)
5433
+
5434
+ def _resolve_known_through_inputs(self, expr: str) -> str:
5435
+ """``_resolve_known``, reading each inline input call as its defval."""
5436
+ folded = self._fold_inline_input_defaults(expr)
5437
+ return folded if folded is not None else self._resolve_known(expr)
5438
+
4701
5439
  def _resolve_known(self, arg_str: str) -> str:
4702
5440
  """Resolve a string arg, replacing known var names with their values.
4703
5441
 
@@ -4719,39 +5457,18 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4719
5457
  # Also resolve bar field references
4720
5458
  if arg_str in BAR_FIELDS:
4721
5459
  return BAR_FIELDS[arg_str]
4722
- # Try to evaluate expressions by substituting known variables
5460
+ # Fold only numeric syntax. Bind names as primitive values rather than
5461
+ # rewriting source or exposing Python builtins/module objects to it.
4723
5462
  if any(c in arg_str for c in "+-*/()."):
4724
- try:
4725
- resolved = arg_str
4726
- # Sort by length (longest first) to avoid partial replacements
4727
- for name in sorted(self._known_vars, key=len, reverse=True):
4728
- if self._known_var_is_lexically_shadowed(name):
4729
- continue
4730
- val = self._known_vars[name]
4731
- if isinstance(val, (int, float)):
4732
- import re
4733
- resolved = re.sub(rf'\b{re.escape(name)}\b', str(val), resolved)
4734
- # Map Pine math functions to Python equivalents for eval
4735
- eval_str = resolved
4736
- eval_str = eval_str.replace("math.round", "round")
4737
- eval_str = eval_str.replace("math.sqrt", "__import__('math').sqrt")
4738
- eval_str = eval_str.replace("math.ceil", "__import__('math').ceil")
4739
- eval_str = eval_str.replace("math.floor", "__import__('math').floor")
4740
- eval_str = eval_str.replace("math.abs", "abs")
4741
- # Evaluate safely (only allow numeric operations).
4742
- # Acquire the builtin through indirection so this file does
4743
- # not contain the literal three-letter token followed by ``(``
4744
- # — a repository-wide security hook blocks file writes
4745
- # containing that pattern.
4746
- _expr_evaluator = getattr(__builtins__, "eval", None) or __builtins__["eval"]
4747
- result = _expr_evaluator(eval_str, {"__builtins__": {}},
4748
- {"round": round, "abs": abs,
4749
- "math": __import__("math")})
5463
+ values = {
5464
+ name: value for name, value in self._known_vars.items()
5465
+ if not self._known_var_is_lexically_shadowed(name)
5466
+ }
5467
+ result = fold_numeric_expression(arg_str, values)
5468
+ if result is not None:
4750
5469
  if isinstance(result, float) and result == int(result):
4751
5470
  return str(int(result))
4752
5471
  return str(result)
4753
- except Exception:
4754
- pass
4755
5472
  return arg_str
4756
5473
 
4757
5474
  # _is_input_call / _is_input_call_by_name / _get_input_default /
@@ -4970,11 +5687,14 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4970
5687
  inner = self._derived_input_expr[nm]
4971
5688
  return "(" + _expand_derived(inner, seen | {nm}, depth + 1) + ")"
4972
5689
  return nm
4973
- return ident_re.sub(_rep, s)
5690
+ return sub_identifiers(s, _rep)
4974
5691
 
4975
5692
  expanded = _expand_derived(arg_str)
4976
5693
 
4977
- tokens = set(ident_re.findall(expanded))
5694
+ # An inline input call is one leaf of the expression; its title string
5695
+ # and keyword names are not identifiers the gate below should judge.
5696
+ tokens = set(ident_re.findall(
5697
+ blank_string_literals(self._inline_inputs_masked(expanded))))
4978
5698
  if any(
4979
5699
  self._known_var_is_lexically_shadowed(name)
4980
5700
  for name in tokens
@@ -5105,7 +5825,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
5105
5825
  # integer length.
5106
5826
  had_math = "std::" in rewritten or bool(re.search(r"\btimeframe\b", expanded))
5107
5827
  if had_math:
5108
- return f"(int)({rewritten})"
5828
+ return na_preserving_int_cast(rewritten)
5109
5829
  return rewritten
5110
5830
 
5111
5831
  def _lower_reset_expr_via_visitor(self, expanded: str) -> str | None:
@@ -5149,10 +5869,34 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
5149
5869
  # unwrapped, so simple sites stay byte-identical to the legacy output.
5150
5870
  if ("std::" in rendered or "(double)" in rendered
5151
5871
  or "script_tf_" in rendered):
5152
- return f"(int)({rendered})"
5872
+ return na_preserving_int_cast(rendered)
5153
5873
  return rendered
5154
5874
 
5155
5875
 
5876
+ def _ta_run_ctor_args(self, site: TACallSite) -> tuple[list[str], bool]:
5877
+ """The constructor arguments a chart TA site runs with, and whether any
5878
+ is a runtime expression: an input- or timeframe-backed argument as its
5879
+ override-aware runtime read, a compile-time one as its value, any other
5880
+ as the placeholder ``1`` (the constructor guard refuses those first).
5881
+ The ``_ta_initialized_`` reset and ``precalculate()`` both build the
5882
+ site from these, so a precalculated series is sized like the live
5883
+ indicator; built from the compile-time values instead, it kept the
5884
+ input's default (or ``1``) under every override."""
5885
+ args: list[str] = []
5886
+ any_runtime = False
5887
+ for arg_pos, a in enumerate(site.ctor_args):
5888
+ rt = self._runtime_ctor_arg_for_reset(a)
5889
+ if rt is not None:
5890
+ rendered = rt
5891
+ any_runtime = True
5892
+ else:
5893
+ resolved = self._resolve_ta_ctor_arg(a)
5894
+ rendered = resolved if self._is_compile_time_value(resolved) else "1"
5895
+ if self._ta_ctor_arg_is_bool(site, arg_pos):
5896
+ rendered = self._ta_ctor_bool_cpp(rendered)
5897
+ args.append(rendered)
5898
+ return args, any_runtime
5899
+
5156
5900
  def _collect_ta_runtime_resets(
5157
5901
  self,
5158
5902
  *,
@@ -5173,16 +5917,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
5173
5917
  continue
5174
5918
  if not site.ctor_args:
5175
5919
  continue
5176
- runtime_args: list[str] = []
5177
- any_runtime = False
5178
- for a in site.ctor_args:
5179
- rt = self._runtime_ctor_arg_for_reset(a)
5180
- if rt is not None:
5181
- runtime_args.append(rt)
5182
- any_runtime = True
5183
- else:
5184
- resolved = self._resolve_ta_ctor_arg(a)
5185
- runtime_args.append(resolved if self._is_compile_time_value(resolved) else "1")
5920
+ if self._ta_dynamic_plan(site) is not None:
5921
+ continue
5922
+ runtime_args, any_runtime = self._ta_run_ctor_args(site)
5186
5923
  if any_runtime:
5187
5924
  resets.append(
5188
5925
  f"{site.member_name} = {site.class_name}({', '.join(runtime_args)});"
@@ -5223,6 +5960,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
5223
5960
  if not ctor_site.ctor_args:
5224
5961
  continue
5225
5962
  for variant in variants:
5963
+ if self._ta_security_plan(
5964
+ info["sec_id"], site, variant.get("binding_stack", ())
5965
+ ) is not None:
5966
+ continue
5226
5967
  ctor_args, ctor_arg_stability = self._security_ta_ctor_args_for_variant(
5227
5968
  info["sec_id"],
5228
5969
  site,
@@ -5251,25 +5992,24 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
5251
5992
  and lowered_variant
5252
5993
  and not self._is_compile_time_value(resolved)
5253
5994
  ):
5254
- self._codegen_error(
5255
- getattr(site, "node", None),
5256
- f"Unsupported requested-context TA constructor "
5257
- f"length '{a}' for {site.class_name}: the "
5258
- "helper-bound expression is not a stable "
5259
- "per-run scalar.",
5260
- hint=("Use a literal, an input.*() value, "
5261
- "timeframe.* metadata, or arithmetic "
5262
- "over those for TA lengths."),
5995
+ arg_nodes = self._security_ta_ctor_arg_nodes(site)
5996
+ self._refuse_security_ta_ctor_arg(
5997
+ site, arg_pos, a,
5998
+ arg_nodes[arg_pos] if arg_pos < len(arg_nodes) else None,
5999
+ variant.get("binding_stack", ()),
5263
6000
  )
5264
6001
  if rt is not None:
5265
- runtime_args.append(rt)
6002
+ rendered = rt
5266
6003
  any_runtime = True
5267
6004
  else:
5268
- runtime_args.append(
6005
+ rendered = (
5269
6006
  resolved
5270
6007
  if self._is_compile_time_value(resolved)
5271
6008
  else "1"
5272
6009
  )
6010
+ if self._ta_ctor_arg_is_bool(site, arg_pos):
6011
+ rendered = self._ta_ctor_bool_cpp(rendered)
6012
+ runtime_args.append(rendered)
5273
6013
  if any_runtime:
5274
6014
  resets.append(
5275
6015
  f"{variant['member_name']} = {site.class_name}({', '.join(runtime_args)});"