@pineforge/codegen-pyodide 0.10.3 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) 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 +767 -77
  7. package/pineforge_codegen/analyzer/call_handlers.py +268 -42
  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 +907 -159
  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 +961 -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 +1198 -101
  25. package/pineforge_codegen/codegen/tables.py +205 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +2055 -62
  28. package/pineforge_codegen/codegen/visit_call.py +929 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +765 -63
  30. package/pineforge_codegen/codegen/visit_stmt.py +610 -52
  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/symbols.py +4 -2
  47. package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
  48. package/release.json +2 -2
  49. package/tables.json +23 -21
  50. package/transpile.worker.mjs +24 -16
  51. package/pineforge_codegen-0.10.3.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,
@@ -1914,7 +2146,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1914
2146
  self._collection_types[name] = spec
1915
2147
  elif ns == "array" and fn in ({"new", "from"} | set(ARRAY_NEW_CTORS)):
1916
2148
  self._array_vars.add(name)
1917
- spec = self._type_spec_from_expr(expr) or self._array_spec_for_name(name)
2149
+ spec = self._widen_array_spec_for_name(
2150
+ name,
2151
+ self._type_spec_from_expr(expr) or self._array_spec_for_name(name),
2152
+ )
1918
2153
  self._collection_types[name] = spec
1919
2154
  elif ns == "map" and fn == "new":
1920
2155
  self._map_vars.add(name)
@@ -1934,7 +2169,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1934
2169
  fn2, ns2 = self._resolve_callee(expr.callee)
1935
2170
  if ns2 == "array" and fn2 in ({"new", "from"} | set(ARRAY_NEW_CTORS)):
1936
2171
  self._array_vars.add(name)
1937
- spec2 = self._type_spec_from_expr(expr) or self._array_spec_for_name(name)
2172
+ spec2 = self._widen_array_spec_for_name(
2173
+ name,
2174
+ self._type_spec_from_expr(expr) or self._array_spec_for_name(name),
2175
+ )
1938
2176
  self._collection_types[name] = spec2
1939
2177
  continue
1940
2178
  if name in self._matrix_specs:
@@ -2263,10 +2501,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2263
2501
  all_args.append(None)
2264
2502
  all_args[i] = node.kwargs[pname]
2265
2503
 
2266
- if func_name == "highest" and len(all_args) == 1:
2267
- all_args = [Identifier(name="high"), all_args[0]]
2268
- elif func_name == "lowest" and len(all_args) == 1:
2269
- 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]]
2270
2509
 
2271
2510
  return all_args
2272
2511
 
@@ -2310,6 +2549,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2310
2549
  rejected by the constructor guard. Names are NOT folded into
2311
2550
  ``_known_vars`` (no use-site inlining): only the length-analysis path is
2312
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``).
2313
2555
  """
2314
2556
  literal_candidates = self._admitted_stable_var_ctor_literals(reassigned)
2315
2557
 
@@ -2323,6 +2565,19 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2323
2565
  continue
2324
2566
  if self._decl_binding_is_series(id(stmt), stmt.name):
2325
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
2326
2581
  if stmt.value is None or not self._expr_is_stable(stmt.value):
2327
2582
  continue
2328
2583
  expr_str = self._arith_expr_to_str(stmt.value)
@@ -2334,9 +2589,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2334
2589
  self._stable_var_ctor_literals[stmt.name] = literal_record
2335
2590
  # Mark input-backed iff the init references an input, so the reset
2336
2591
  # emits override-aware get_input_*() reads for it.
2337
- import re as _re
2338
- toks = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", expr_str))
2339
- if any(t in self._input_backed_vars for t in toks):
2592
+ if self._refs_input(expr_str):
2340
2593
  self._input_backed_vars.add(stmt.name)
2341
2594
 
2342
2595
  @staticmethod
@@ -2650,7 +2903,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2650
2903
  # Fold to a compile-time literal when possible (so the ctor-init
2651
2904
  # list can use it directly); otherwise record the raw expression
2652
2905
  # for the runtime reset path to expand.
2653
- folded = self._resolve_known(final)
2906
+ folded = self._resolve_known_through_inputs(final)
2654
2907
  if self._is_compile_time_value(folded):
2655
2908
  try:
2656
2909
  num = float(folded)
@@ -2663,9 +2916,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2663
2916
  self._stable_runtime_vars.add(target_name)
2664
2917
  # Mark input-backed iff the expression references an input so the
2665
2918
  # override-aware get_input_*() reads are emitted on the reset path.
2666
- import re as _re
2667
- toks = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", final))
2668
- if any(t in self._input_backed_vars for t in toks):
2919
+ if self._refs_input(final):
2669
2920
  self._input_backed_vars.add(target_name)
2670
2921
 
2671
2922
  def _find_reassigned_vars(self) -> set[str]:
@@ -2910,6 +3161,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2910
3161
  for a in node.args)
2911
3162
  if namespace == "input":
2912
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)
2913
3170
  if namespace is None and func_name in ("int", "float", "bool", "string"):
2914
3171
  return all(self._expr_is_stable(a, _udf_stack, _depth)
2915
3172
  for a in node.args)
@@ -2942,7 +3199,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2942
3199
  # tree: Pine grouping ``(a - b) / (c - d)`` degrades to ``a - b / c - d``
2943
3200
  # under C++ precedence. See ``_runtime_ctor_arg_for_reset`` (the string is
2944
3201
  # re-parsed and lowered through the expression visitor).
2945
- _ATOMIC_ARITH_NODES = (NumberLiteral, Identifier, MemberAccess, FuncCall)
3202
+ _ATOMIC_ARITH_NODES = (NumberLiteral, StringLiteral, Identifier, MemberAccess, FuncCall)
2946
3203
 
2947
3204
  def _arith_operand_to_str(self, node, _udf_stack: frozenset = frozenset(),
2948
3205
  _depth: int = 0) -> str | None:
@@ -2967,11 +3224,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2967
3224
  against recursion cycles (Pine forbids recursion, but a malformed source
2968
3225
  must be refused, not looped forever).
2969
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
2970
3233
  if isinstance(node, NumberLiteral):
2971
3234
  v = node.value
2972
3235
  if isinstance(v, float) and v == int(v):
2973
3236
  return str(int(v))
2974
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)
2975
3242
  if isinstance(node, Identifier):
2976
3243
  return node.name
2977
3244
  if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
@@ -3003,12 +3270,27 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3003
3270
  # ordinary ``callee(args)`` rendering below. The stack/depth guard
3004
3271
  # refuses a recursive UDF (returns None -> caller leaves it untracked
3005
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))
3006
3278
  fn, ns = self._resolve_callee(node.callee)
3007
3279
  if ns is None and fn is not None and self._get_udf_def(fn) is not None:
3008
- inlined = self._inline_single_expr_udf(node, _udf_stack, _depth)
3009
- if inlined is None:
3010
- return None
3011
- 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]
3012
3294
  callee = self._arith_expr_to_str(node.callee, _udf_stack, _depth)
3013
3295
  if callee is None:
3014
3296
  return None
@@ -3021,14 +3303,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3021
3303
  return f"{callee}({', '.join(parts)})"
3022
3304
  return None
3023
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
+
3024
3312
  def _collect_known_var(self, node: VarDecl) -> None:
3025
3313
  """Extract known constant value from a VarDecl."""
3026
3314
  # Don't inline series variables — their values change over time
3027
3315
  if self._decl_binding_is_series(id(node), node.name):
3028
3316
  return
3029
3317
  # Don't inline var/varip variables — they're mutable state that persists
3030
- # across bars and can be reassigned with :=
3031
- 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):
3032
3321
  return
3033
3322
  if isinstance(node.value, NumberLiteral):
3034
3323
  self._known_vars[node.name] = node.value.value
@@ -3043,6 +3332,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3043
3332
  self._input_backed_vars.add(node.name)
3044
3333
  if node.value.name in self._input_var_to_call:
3045
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)
3046
3350
  if node.value.name in self._timeframe_period_vars:
3047
3351
  self._timeframe_period_vars.add(node.name)
3048
3352
  elif (isinstance(node.value, MemberAccess)
@@ -3052,25 +3356,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3052
3356
  self._timeframe_period_vars.add(node.name)
3053
3357
  # Input calls: extract default value
3054
3358
  elif isinstance(node.value, FuncCall) and self._is_input_call(node.value):
3055
- default = self._get_input_default(node.value)
3056
- stored = False
3057
- if isinstance(default, NumberLiteral):
3058
- self._known_vars[node.name] = default.value
3059
- stored = True
3060
- elif isinstance(default, BoolLiteral):
3061
- self._known_vars[node.name] = default.value
3062
- stored = True
3063
- elif isinstance(default, StringLiteral):
3064
- self._known_vars[node.name] = default.value
3065
- stored = True
3066
- elif isinstance(default, MemberAccess) and isinstance(default.object, Identifier):
3067
- en = default.object.name
3068
- if en in self._enum_defs and default.member in self._enum_defs[en]:
3069
- self._known_vars[node.name] = self._enum_defs[en].index(
3070
- default.member
3071
- )
3072
- stored = True
3359
+ stored, default = self._input_default_value(node.value)
3073
3360
  if stored:
3361
+ self._known_vars[node.name] = default
3074
3362
  self._input_backed_vars.add(node.name)
3075
3363
  self._input_var_to_call[node.name] = node.value
3076
3364
  # Class-scope arithmetic / ternaries / casts over known, input-backed,
@@ -3093,8 +3381,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3093
3381
  expr_str = self._arith_expr_to_str(node.value)
3094
3382
  if expr_str is not None and self._expr_is_stable(node.value):
3095
3383
  import re as _re
3096
- tokens = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", expr_str))
3097
- 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)
3098
3388
  refs_derived = any(t in self._derived_input_expr for t in tokens)
3099
3389
  # The stability classifier already proved this expression is a
3100
3390
  # bar-invariant scalar (inputs / constants / timeframe.* /
@@ -3102,7 +3392,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3102
3392
  # stable exprs (and the TA reset path) can reference / expand
3103
3393
  # it — e.g. ``pi = math.asin(1) * 2`` feeds ``beta`` feeds
3104
3394
  # ``alpha`` feeds a function-local ``filterLen``.
3105
- folded = self._resolve_known(expr_str)
3395
+ folded = self._resolve_known_through_inputs(expr_str)
3106
3396
  if self._is_compile_time_value(folded):
3107
3397
  try:
3108
3398
  num = float(folded)
@@ -3128,25 +3418,38 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3128
3418
  # Public entry point
3129
3419
  # ------------------------------------------------------------------
3130
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
+
3131
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:
3132
3437
  loc = node.loc if node is not None else None
3133
3438
  if loc is None:
3134
3439
  loc = SourceLocation(file=self.ctx.filename, line=1, col=1, end_col=1)
3135
- raise CompileError(
3136
- [
3137
- Diagnostic(
3138
- level=Level.ERROR,
3139
- phase=Phase.CODEGEN,
3140
- location=loc,
3141
- message=message,
3142
- hint=hint,
3143
- )
3144
- ]
3440
+ return Diagnostic(
3441
+ level=Level.ERROR,
3442
+ phase=Phase.CODEGEN,
3443
+ location=loc,
3444
+ message=message,
3445
+ hint=hint,
3145
3446
  )
3146
3447
 
3147
3448
  def _ta_return_type(self, site: TACallSite) -> str:
3148
3449
  if getattr(site, "returns_tuple", False):
3149
- 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
+ )
3150
3453
  if site.class_name in ("ta::Crossover", "ta::Crossunder", "ta::Cross"):
3151
3454
  return "bool"
3152
3455
  return "double"
@@ -3229,11 +3532,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3229
3532
  """
3230
3533
  self._inline_history_members = []
3231
3534
  self._inline_history_member_by_key = {}
3232
- counters = {
3233
- "hist_call": 0,
3234
- "series_arg": 0,
3235
- "udf_series_arg": 0,
3236
- }
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", ()))
3237
3544
 
3238
3545
  def walk_nodes(value):
3239
3546
  """Yield AST nodes in stable field order, including tuple elements.
@@ -3329,6 +3636,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3329
3636
  return
3330
3637
  counters[kind] += 1
3331
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]}"
3332
3642
  self._inline_history_member_by_key[key] = member_name
3333
3643
  self._inline_history_members.append({
3334
3644
  "kind": kind,
@@ -3375,6 +3685,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3375
3685
  return list(
3376
3686
  self._bind_typed_method_args(method_info, call).args_by_param
3377
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
3378
3694
  if call.kwargs:
3379
3695
  return _merge_kwargs(call.args, call.kwargs, params, lambda arg: arg)
3380
3696
  return list(call.args)
@@ -3430,6 +3746,20 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3430
3746
  id(child) for child in walk_nodes(expression)
3431
3747
  )
3432
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
+ }
3433
3763
 
3434
3764
  def emitted_context_for_call(
3435
3765
  fi,
@@ -3518,6 +3848,27 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3518
3848
  register(
3519
3849
  "hist_call", (id(node),), self._infer_type(node.object), owner
3520
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
+ )
3521
3872
 
3522
3873
  if not isinstance(node, FuncCall):
3523
3874
  continue
@@ -3638,12 +3989,155 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3638
3989
  context,
3639
3990
  )
3640
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
+
3641
4125
  def _inline_history_member(self, kind: str, node: ASTNode,
3642
4126
  arg_idx: int | None = None) -> str:
3643
4127
  source_key = (id(node),) if arg_idx is None else (id(node), arg_idx)
3644
4128
  key = (kind, *source_key, self._current_instance_name)
3645
4129
  member = self._inline_history_member_by_key.get(key)
3646
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
+ )
3647
4141
  raise AssertionError(
3648
4142
  "missing pre-registered inline history member for "
3649
4143
  f"{kind} at {getattr(node, 'loc', None)} in context "
@@ -3785,8 +4279,47 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3785
4279
  self._udt_arena_member_names[type_name] = allocated
3786
4280
  self._all_member_names.add(allocated)
3787
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
+
3788
4317
  def generate(self) -> str:
3789
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()
3790
4323
  # Context-sensitive instance pre-pass (needs the naming helpers populated
3791
4324
  # in __init__). Computes nested stateful-helper dispatch + fresh instances.
3792
4325
  self._build_func_instances()
@@ -3800,14 +4333,26 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3800
4333
  self._prepare_udt_generated_names()
3801
4334
  # Pre-scan for strategy series vars
3802
4335
  self._prescan_strategy_series()
4336
+ self._prescan_session_history()
3803
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] = {}
3804
4341
  # request.security TA call-sites read at a history offset (``ta.ema(...)[k>=1]``).
3805
4342
  # Maps sec_id -> set of TA call-site indices needing an HTF history Series.
3806
4343
  self._security_ta_hist_idx_by_sec: dict[int, set[int]] = {}
3807
4344
  # request.security helper-call results read at a history offset
3808
4345
  # (``myHelper()[k]``). Maps (sec_id, node-id) -> backing Series metadata.
3809
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()
3810
4354
  self._prepare_lazy_source_clock_sites()
4355
+ self.block_locals_needing_names = self._block_locals_needing_names()
3811
4356
 
3812
4357
  lines: list[str] = []
3813
4358
 
@@ -3829,11 +4374,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3829
4374
  # The two-phase order also permits self/nested UDT fields without
3830
4375
  # embedding a C++ type recursively by value.
3831
4376
  for type_name in self._udt_defs:
3832
- lines.append(f"struct {type_name} {{")
4377
+ safe_type_name = self._safe_name(type_name)
4378
+ lines.append(f"struct {safe_type_name} {{")
3833
4379
  lines.append(" int32_t __pf_id = -1;")
3834
4380
  lines.append("};")
3835
4381
  lines.append(
3836
- f"inline bool is_na(const {type_name}& _z) "
4382
+ f"inline bool is_na(const {safe_type_name}& _z) "
3837
4383
  "{ return _z.__pf_id < 0; }"
3838
4384
  )
3839
4385
  lines.append("")
@@ -3859,6 +4405,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3859
4405
  f" {self._udt_undo_coordinator_cpp_name}& operator=(",
3860
4406
  f" {self._udt_undo_coordinator_cpp_name}&&) = delete;",
3861
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
+ "",
3862
4414
  " Snapshot snapshot() {",
3863
4415
  " if (_pf_generation_ == std::numeric_limits<uint64_t>::max()) {",
3864
4416
  ' throw std::overflow_error("UDT checkpoint generation exhausted");',
@@ -3936,6 +4488,14 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3936
4488
  f" {self._udt_arena_template_cpp_name}& operator=(",
3937
4489
  f" {self._udt_arena_template_cpp_name}&&) = delete;",
3938
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
+ "",
3939
4499
  " _PFHandle create(_PFRecord value) {",
3940
4500
  " if (_pf_records_.size() > static_cast<std::size_t>(",
3941
4501
  " std::numeric_limits<int32_t>::max())) {",
@@ -4006,6 +4566,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4006
4566
  # the same map. Authored field defaults are evaluated by ``Type.new``
4007
4567
  # at the call site; record declarations use only inert typed defaults so
4008
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)
4009
4571
  for type_name, fields in self._udt_defs.items():
4010
4572
  record_type = self._udt_record_cpp_type(type_name)
4011
4573
  lines.append(f"struct {record_type} {{")
@@ -4015,11 +4577,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4015
4577
  if f.name in omitted:
4016
4578
  continue
4017
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
4018
4584
  cpp_type = self._type_spec_to_cpp(spec)
4019
4585
  if cpp_type == "int":
4020
4586
  cpp_type = "int64_t"
4021
4587
  default = self._default_for_spec(spec)
4022
- lines.append(f" {cpp_type} {f.name} = {default};")
4588
+ lines.append(f" {cpp_type} {self._safe_name(f.name)} = {default};")
4023
4589
  lines.append("};")
4024
4590
  lines.append("")
4025
4591
 
@@ -4028,15 +4594,18 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4028
4594
 
4029
4595
  # 1c. Enum constants + string tables for str.tostring(enumVar)
4030
4596
  for enum_name, members in self._enum_defs.items():
4597
+ safe_enum_name = self._safe_name(enum_name)
4031
4598
  for i, member in enumerate(members):
4032
- 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
+ )
4033
4602
  strs = self._enum_member_strings.get(enum_name)
4034
4603
  if strs and len(strs) == len(members):
4035
4604
  parts = ", ".join(
4036
4605
  f'std::string("{self._cpp_string_escape(s)}")' for s in strs
4037
4606
  )
4038
4607
  lines.append(
4039
- f"static const std::string {enum_name}_str_values[] = {{{parts}}};"
4608
+ f"static const std::string {safe_enum_name}_str_values[] = {{{parts}}};"
4040
4609
  )
4041
4610
  lines.append("")
4042
4611
 
@@ -4045,9 +4614,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4045
4614
  # per-callsite instances are declared below inside GeneratedStrategy
4046
4615
  # and therefore join the automatic COOF checkpoint inventory.
4047
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)
4048
4620
 
4049
4621
  # 2. Open class
4050
- lines.append("class GeneratedStrategy : public BacktestEngine {")
4622
+ lines.append("class GeneratedStrategy : public pineforge::source::PineStrategyHost {")
4051
4623
  lines.append("public:")
4052
4624
  _script_state_decl_start = len(lines)
4053
4625
 
@@ -4121,6 +4693,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4121
4693
  self._security_ohlc_hist_fields_by_sec[sec_id] = hist_fields
4122
4694
  for i, el in enumerate(expr_node.elements):
4123
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"
4124
4702
  if ctype == "std::vector<double>":
4125
4703
  lines.append(f" {ctype} _req_sec_{sec_id}_{i}{{}};")
4126
4704
  else:
@@ -4156,7 +4734,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4156
4734
  self._security_ohlc_hist_fields_by_sec[sec_id] = (
4157
4735
  self._collect_security_ohlc_hist_fields_for_call(item)
4158
4736
  )
4159
- 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>();")
4160
4741
  for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
4161
4742
  ctype = self._security_bar_hist_type(field)
4162
4743
  lines.append(
@@ -4168,9 +4749,22 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4168
4749
  for name in self._security_ta_hist_series_names(sec_id):
4169
4750
  lines.append(f" Series<double> {name}{_mbb};")
4170
4751
  self._emit_security_expr_hist_members(sec_id, expr_node, lines, _mbb)
4171
-
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;")
4172
4760
  if self._security_calls:
4173
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
+ )
4174
4768
 
4175
4769
  # Security-local mutable global state for request.security
4176
4770
  for info in self._security_eval_info:
@@ -4194,7 +4788,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4194
4788
  for _ta_idx, site in enumerate(self.ctx.ta_call_sites):
4195
4789
  if _ta_idx in self._dead_ta_indices:
4196
4790
  continue
4197
- lines.append(f" {site.class_name} {site.member_name};")
4791
+ lines.append(f" {self._ta_member_cpp_type(site)} {site.member_name};")
4198
4792
  if self._ta_site_uses_precalc(site):
4199
4793
  vtype = self._ta_return_type(site)
4200
4794
  lines.append(f" std::vector<{vtype}> _precalc_{site.member_name};")
@@ -4213,8 +4807,6 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4213
4807
  literal = info["length_literal"]
4214
4808
  capacity = f"{{{literal + 1}}}" if literal is not None and literal >= 1 else ""
4215
4809
  lines.append(f" Series<double> {info['hist']}{capacity};")
4216
- if info["chart"] is not None:
4217
- lines.append(f" Series<double> {info['chart']}{capacity};")
4218
4810
 
4219
4811
  # Security evaluator TA members (cloned from expression dependencies)
4220
4812
  # Skip for user function call expressions — their TA deps are internal to the function
@@ -4222,18 +4814,26 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4222
4814
  for idx, variants in (info.get("ta_variants") or {}).items():
4223
4815
  site = self.ctx.ta_call_sites[idx]
4224
4816
  for variant in variants:
4225
- 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']};")
4226
4819
 
4227
4820
  # 4. Series members for bar field history
4228
4821
  for field_name in sorted(self.ctx.series_bar_fields):
4229
4822
  lines.append(f" Series<double> _s_{field_name}{_mbb};")
4230
4823
 
4231
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)
4232
4829
  seen_var_members: set[str] = set()
4233
4830
  for name, ptype, init_str in self.ctx.var_members:
4234
4831
  if name in seen_var_members:
4235
4832
  continue
4236
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)
4237
4837
  safe = self._safe_name(name)
4238
4838
  callable_collection_spec = (
4239
4839
  None
@@ -4275,7 +4875,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4275
4875
  )
4276
4876
  ):
4277
4877
  self._array_vars.add(name)
4278
- array_spec = exact_member_spec or self._array_spec_for_name(name)
4878
+ array_spec = self._widen_array_spec_for_name(
4879
+ name, exact_member_spec or self._array_spec_for_name(name)
4880
+ )
4279
4881
  lines.append(
4280
4882
  f" {self._type_spec_to_cpp(array_spec)} {safe};"
4281
4883
  )
@@ -4339,7 +4941,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4339
4941
  udt_type = udt_name
4340
4942
  break
4341
4943
  if udt_type:
4342
- lines.append(f" {udt_type} {safe};")
4944
+ lines.append(f" {self._safe_name(udt_type)} {safe};")
4343
4945
  continue
4344
4946
  cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
4345
4947
  # Promote int->int64_t when init RHS is an int64-returning builtin
@@ -4379,6 +4981,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4379
4981
  cpp_type = self._series_type_for(name)
4380
4982
  lines.append(f" Series<{cpp_type}> {safe}{_mbb};")
4381
4983
 
4984
+ _variable_decls = lines[_variable_decls_start:]
4382
4985
  # 7. Fixnan members
4383
4986
  for _fi_idx, site in enumerate(self.ctx.fixnan_sites):
4384
4987
  if _fi_idx in self._dead_fixnan_indices:
@@ -4396,6 +4999,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4396
4999
  else:
4397
5000
  lines.append(f" Series<double> {svar}{_mbb};")
4398
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
+
4399
5007
  # 8a. Synthetic temporary history. Unlike the legacy function-local
4400
5008
  # static buffers, these members are value-copyable rollback state and
4401
5009
  # have one identity per source site / emitted UDF variant.
@@ -4442,10 +5050,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4442
5050
  if _draw_cpp is not None:
4443
5051
  lines.append(f" {_draw_cpp} {safe} = {_draw_cpp}{{}};")
4444
5052
  else:
4445
- 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}{{}};")
4446
5055
  else:
4447
5056
  expr = self.ctx.global_expr_map.get(name) if hasattr(self.ctx, "global_expr_map") else None
4448
- 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 (
4449
5062
  name in self._direct_program_tuple_binding_names
4450
5063
  and ptype == PineType.BOOL
4451
5064
  ):
@@ -4454,15 +5067,39 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4454
5067
  # coarse request.security call expression itself still
4455
5068
  # reports FLOAT, so prefer that exact binding type here.
4456
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"
4457
5085
  else:
4458
5086
  cpp_type = (
4459
5087
  self._infer_type(expr)
4460
5088
  if expr is not None
4461
5089
  else PINE_TYPE_TO_CPP.get(ptype, "double")
4462
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"
4463
5099
  default = self._default_for_type(cpp_type)
4464
5100
  lines.append(f" {cpp_type} {safe} = {default};")
4465
5101
 
5102
+ _variable_decls_start = len(lines)
4466
5103
  # 8c. Cloned var/series members for per-call-site function variants
4467
5104
  # Same pattern as TA member cloning: each call site gets its own copy
4468
5105
  emitted_clones: set[str] = set()
@@ -4488,6 +5125,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4488
5125
  orig_safe, fresh_safe, _mbb, lines, owner_func=owner_func
4489
5126
  )
4490
5127
 
5128
+ _variable_decls = _variable_decls + lines[_variable_decls_start:]
5129
+ self._drawing_pins = self._drawing_pin_members(_variable_decls)
5130
+
4491
5131
  # 8c3. Fresh fixnan members for context-sensitive helper instances.
4492
5132
  # Each fresh instance gets its OWN previous-value member so two
4493
5133
  # call paths never share fixnan state (mirrors 8c2 for vars).
@@ -4500,14 +5140,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4500
5140
 
4501
5141
  # 8d. Drawing-objects-as-data arenas (gated on _uses_drawing so
4502
5142
  # non-drawing strategies emit byte-identical C++). Each arena is a
4503
- # per-strategy member -> reset-per-run is automatic. Caps come from
4504
- # 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.
4505
5147
  if self._uses_drawing:
4506
- caps = self._drawing_caps or {}
4507
- lines.append(f" DrawingArena<LineRec> _pf_lines_{{{caps.get('line', 50)}}};")
4508
- lines.append(f" DrawingArena<BoxRec> _pf_boxes_{{{caps.get('box', 50)}}};")
4509
- lines.append(f" DrawingArena<LabelRec> _pf_labels_{{{caps.get('label', 50)}}};")
4510
- 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};")
4511
5152
 
4512
5153
  # 9. _var_initialized flag
4513
5154
  if self.ctx.var_members:
@@ -4569,15 +5210,23 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4569
5210
  # inventory from the declarations above so every future generated
4570
5211
  # state category is captured automatically (or generation fails loudly
4571
5212
  # if it introduces an unfamiliar declaration form).
5213
+ _script_state_declarations = lines[_script_state_decl_start:-1]
4572
5214
  _script_state_members = self._collect_script_state_members(
4573
- lines[_script_state_decl_start:-1]
5215
+ _script_state_declarations
4574
5216
  )
4575
5217
  self._emit_script_state_hooks(lines, _script_state_members)
4576
5218
  lines.append("")
5219
+ # Its per-strategy cache, outside the checkpointed script state.
5220
+ _session_market_member_at = len(lines)
4577
5221
 
4578
5222
  # 9. Constructor with TA initializer list
4579
5223
  self._emit_constructor(lines)
4580
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("")
4581
5230
 
4582
5231
  # 10. User-defined functions (with per-call-site variants for functions
4583
5232
  # containing TA calls OR series variables that need isolation)
@@ -4639,7 +5288,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4639
5288
  # 13. extern "C" interface
4640
5289
  self._emit_extern_c(lines)
4641
5290
 
4642
- 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
4643
5298
 
4644
5299
  # ------------------------------------------------------------------
4645
5300
  # Top-level emitters (_emit_includes / _emit_constructor / _emit_on_bar
@@ -4688,8 +5343,99 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4688
5343
  and not self._known_var_is_lexically_shadowed(arg_str)
4689
5344
  ):
4690
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
4691
5352
  return resolved
4692
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
+
4693
5439
  def _resolve_known(self, arg_str: str) -> str:
4694
5440
  """Resolve a string arg, replacing known var names with their values.
4695
5441
 
@@ -4711,39 +5457,18 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4711
5457
  # Also resolve bar field references
4712
5458
  if arg_str in BAR_FIELDS:
4713
5459
  return BAR_FIELDS[arg_str]
4714
- # 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.
4715
5462
  if any(c in arg_str for c in "+-*/()."):
4716
- try:
4717
- resolved = arg_str
4718
- # Sort by length (longest first) to avoid partial replacements
4719
- for name in sorted(self._known_vars, key=len, reverse=True):
4720
- if self._known_var_is_lexically_shadowed(name):
4721
- continue
4722
- val = self._known_vars[name]
4723
- if isinstance(val, (int, float)):
4724
- import re
4725
- resolved = re.sub(rf'\b{re.escape(name)}\b', str(val), resolved)
4726
- # Map Pine math functions to Python equivalents for eval
4727
- eval_str = resolved
4728
- eval_str = eval_str.replace("math.round", "round")
4729
- eval_str = eval_str.replace("math.sqrt", "__import__('math').sqrt")
4730
- eval_str = eval_str.replace("math.ceil", "__import__('math').ceil")
4731
- eval_str = eval_str.replace("math.floor", "__import__('math').floor")
4732
- eval_str = eval_str.replace("math.abs", "abs")
4733
- # Evaluate safely (only allow numeric operations).
4734
- # Acquire the builtin through indirection so this file does
4735
- # not contain the literal three-letter token followed by ``(``
4736
- # — a repository-wide security hook blocks file writes
4737
- # containing that pattern.
4738
- _expr_evaluator = getattr(__builtins__, "eval", None) or __builtins__["eval"]
4739
- result = _expr_evaluator(eval_str, {"__builtins__": {}},
4740
- {"round": round, "abs": abs,
4741
- "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:
4742
5469
  if isinstance(result, float) and result == int(result):
4743
5470
  return str(int(result))
4744
5471
  return str(result)
4745
- except Exception:
4746
- pass
4747
5472
  return arg_str
4748
5473
 
4749
5474
  # _is_input_call / _is_input_call_by_name / _get_input_default /
@@ -4962,11 +5687,14 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4962
5687
  inner = self._derived_input_expr[nm]
4963
5688
  return "(" + _expand_derived(inner, seen | {nm}, depth + 1) + ")"
4964
5689
  return nm
4965
- return ident_re.sub(_rep, s)
5690
+ return sub_identifiers(s, _rep)
4966
5691
 
4967
5692
  expanded = _expand_derived(arg_str)
4968
5693
 
4969
- 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))))
4970
5698
  if any(
4971
5699
  self._known_var_is_lexically_shadowed(name)
4972
5700
  for name in tokens
@@ -5097,7 +5825,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
5097
5825
  # integer length.
5098
5826
  had_math = "std::" in rewritten or bool(re.search(r"\btimeframe\b", expanded))
5099
5827
  if had_math:
5100
- return f"(int)({rewritten})"
5828
+ return na_preserving_int_cast(rewritten)
5101
5829
  return rewritten
5102
5830
 
5103
5831
  def _lower_reset_expr_via_visitor(self, expanded: str) -> str | None:
@@ -5141,10 +5869,34 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
5141
5869
  # unwrapped, so simple sites stay byte-identical to the legacy output.
5142
5870
  if ("std::" in rendered or "(double)" in rendered
5143
5871
  or "script_tf_" in rendered):
5144
- return f"(int)({rendered})"
5872
+ return na_preserving_int_cast(rendered)
5145
5873
  return rendered
5146
5874
 
5147
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
+
5148
5900
  def _collect_ta_runtime_resets(
5149
5901
  self,
5150
5902
  *,
@@ -5165,16 +5917,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
5165
5917
  continue
5166
5918
  if not site.ctor_args:
5167
5919
  continue
5168
- runtime_args: list[str] = []
5169
- any_runtime = False
5170
- for a in site.ctor_args:
5171
- rt = self._runtime_ctor_arg_for_reset(a)
5172
- if rt is not None:
5173
- runtime_args.append(rt)
5174
- any_runtime = True
5175
- else:
5176
- resolved = self._resolve_ta_ctor_arg(a)
5177
- 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)
5178
5923
  if any_runtime:
5179
5924
  resets.append(
5180
5925
  f"{site.member_name} = {site.class_name}({', '.join(runtime_args)});"
@@ -5215,6 +5960,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
5215
5960
  if not ctor_site.ctor_args:
5216
5961
  continue
5217
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
5218
5967
  ctor_args, ctor_arg_stability = self._security_ta_ctor_args_for_variant(
5219
5968
  info["sec_id"],
5220
5969
  site,
@@ -5243,25 +5992,24 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
5243
5992
  and lowered_variant
5244
5993
  and not self._is_compile_time_value(resolved)
5245
5994
  ):
5246
- self._codegen_error(
5247
- getattr(site, "node", None),
5248
- f"Unsupported requested-context TA constructor "
5249
- f"length '{a}' for {site.class_name}: the "
5250
- "helper-bound expression is not a stable "
5251
- "per-run scalar.",
5252
- hint=("Use a literal, an input.*() value, "
5253
- "timeframe.* metadata, or arithmetic "
5254
- "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", ()),
5255
6000
  )
5256
6001
  if rt is not None:
5257
- runtime_args.append(rt)
6002
+ rendered = rt
5258
6003
  any_runtime = True
5259
6004
  else:
5260
- runtime_args.append(
6005
+ rendered = (
5261
6006
  resolved
5262
6007
  if self._is_compile_time_value(resolved)
5263
6008
  else "1"
5264
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)
5265
6013
  if any_runtime:
5266
6014
  resets.append(
5267
6015
  f"{variant['member_name']} = {site.class_name}({', '.join(runtime_args)});"