@pineforge/codegen-pyodide 0.10.4 → 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 (50) hide show
  1. package/README.md +16 -16
  2. package/glue.py +24 -16
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +125 -34
  5. package/pineforge_codegen/analyzer/__init__.py +2 -0
  6. package/pineforge_codegen/analyzer/base.py +754 -76
  7. package/pineforge_codegen/analyzer/call_handlers.py +260 -40
  8. package/pineforge_codegen/analyzer/contracts.py +37 -0
  9. package/pineforge_codegen/analyzer/diagnostics.py +30 -4
  10. package/pineforge_codegen/analyzer/tables.py +49 -8
  11. package/pineforge_codegen/analyzer/types.py +33 -1
  12. package/pineforge_codegen/ast_nodes.py +32 -1
  13. package/pineforge_codegen/block_locals.py +185 -0
  14. package/pineforge_codegen/builtin_keywords.py +42 -0
  15. package/pineforge_codegen/codegen/base.py +896 -156
  16. package/pineforge_codegen/codegen/constant_fold.py +131 -0
  17. package/pineforge_codegen/codegen/drawing.py +221 -79
  18. package/pineforge_codegen/codegen/emit_top.py +946 -213
  19. package/pineforge_codegen/codegen/helpers.py +435 -14
  20. package/pineforge_codegen/codegen/host_members.py +162 -0
  21. package/pineforge_codegen/codegen/input.py +252 -85
  22. package/pineforge_codegen/codegen/security.py +4372 -377
  23. package/pineforge_codegen/codegen/session_market.py +71 -0
  24. package/pineforge_codegen/codegen/ta.py +1188 -100
  25. package/pineforge_codegen/codegen/tables.py +193 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +1882 -78
  28. package/pineforge_codegen/codegen/visit_call.py +920 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +738 -57
  30. package/pineforge_codegen/codegen/visit_stmt.py +595 -49
  31. package/pineforge_codegen/external_requests.py +877 -0
  32. package/pineforge_codegen/lexer.py +104 -22
  33. package/pineforge_codegen/library_inline.py +1304 -0
  34. package/pineforge_codegen/library_modules.py +126 -0
  35. package/pineforge_codegen/library_v5.py +683 -0
  36. package/pineforge_codegen/limits.py +138 -0
  37. package/pineforge_codegen/method_binding.py +33 -0
  38. package/pineforge_codegen/parser.py +384 -68
  39. package/pineforge_codegen/pine_libraries.py +266 -0
  40. package/pineforge_codegen/pine_spelling.py +216 -0
  41. package/pineforge_codegen/pragmas.py +64 -10
  42. package/pineforge_codegen/security_contexts.py +1585 -0
  43. package/pineforge_codegen/session_reads.py +84 -0
  44. package/pineforge_codegen/signatures.py +48 -23
  45. package/pineforge_codegen/support_checker.py +1106 -85
  46. package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
  47. package/release.json +2 -2
  48. package/tables.json +23 -21
  49. package/transpile.worker.mjs +24 -16
  50. package/pineforge_codegen-0.10.4.tar.gz +0 -0
@@ -22,6 +22,8 @@ Sibling-mixin methods consumed via ``self``:
22
22
 
23
23
  from __future__ import annotations
24
24
 
25
+ import copy
26
+ import re
25
27
  from typing import TYPE_CHECKING
26
28
 
27
29
  from ..ast_nodes import (
@@ -31,7 +33,11 @@ from ..ast_nodes import (
31
33
  Ternary, TupleAssign, TupleLiteral, TypeDecl, TypeField, UnaryOp, VarDecl,
32
34
  WhileStmt,
33
35
  )
36
+ from ..pine_spelling import blank_string_literals
37
+ from ..symbols import PineType
38
+ from .helpers import pine_index_int_cast
34
39
  from .tables import (
40
+ BAR_SERIES_PUSH,
35
41
  TA_CHART_PREV_CLOSE,
36
42
  TA_CHART_PREV_CLOSE_ARG,
37
43
  TA_IMPLICIT_APPEND,
@@ -100,7 +106,52 @@ class TaSiteHelper:
100
106
  # .compute() arg-string construction
101
107
  # ------------------------------------------------------------------
102
108
 
109
+ _TA_BOOL_COMPUTE_ARGS = {
110
+ "barssince": frozenset({0}),
111
+ "valuewhen": frozenset({0}),
112
+ "pivot_point_levels": frozenset({1, 2}),
113
+ "vwap_anchored": frozenset({1}),
114
+ "vwap_anchored_bands": frozenset({1}),
115
+ }
116
+
117
+ def _ta_compute_arg_cpp(self, ta_name: str, position: int, node) -> str:
118
+ """Render one TA compute argument with Pine bool truthiness."""
119
+ rendered = self._visit_expr(node)
120
+ if position in self._TA_BOOL_COMPUTE_ARGS.get(ta_name, ()):
121
+ return self._coerce_bool_expr(rendered, node)
122
+ return rendered
123
+
103
124
  def _ta_compute_args_for_site(self, site: "TACallSite") -> str:
125
+ """``_ta_compute_args_for_site_base`` plus, for a site whose length the
126
+ constructor cannot take, the per-call arguments of its lowering
127
+ (``_ta_dynamic_compute_args``)."""
128
+ args = self._ta_compute_args_for_site_base(site)
129
+ # In a user-function body emitted for one call site, the member is
130
+ # that call site's clone: its own constructor arguments decide.
131
+ site = self._ta_active_site(site)
132
+ plan = self._ta_dynamic_plan(site)
133
+ if plan is None:
134
+ return args
135
+ return self._ta_dynamic_compute_args(site, plan, args, self._visit_expr)
136
+
137
+ def _ta_active_site(self, site: "TACallSite") -> "TACallSite":
138
+ """The site whose member ``_ta_member_name`` resolves ``site`` to."""
139
+ remap = self._active_ta_remap
140
+ if not remap:
141
+ return site
142
+ name = remap.get(site.member_name)
143
+ if not name or name == site.member_name:
144
+ return site
145
+ return self._ta_site_by_member().get(name, site)
146
+
147
+ def _ta_site_by_member(self) -> dict:
148
+ by_member = self.__dict__.get("_ta_site_by_member_cache")
149
+ if by_member is None:
150
+ by_member = {s.member_name: s for s in self.ctx.ta_call_sites}
151
+ self.__dict__["_ta_site_by_member_cache"] = by_member
152
+ return by_member
153
+
154
+ def _ta_compute_args_for_site_base(self, site: "TACallSite") -> str:
104
155
  """Build the C++ argument string for ``<member>.compute(...)`` of a TA site.
105
156
 
106
157
  Three layered cases:
@@ -116,6 +167,34 @@ class TaSiteHelper:
116
167
  when applicable (e.g. ``vwma()`` -> ``volume`` only)."""
117
168
  ta_name = self._ta_name_from_site(site)
118
169
 
170
+ # TA1's new stateful wrappers need the Pine anchor after the chart
171
+ # clock arguments. The compatibility shims use the timestamp and
172
+ # symbol clock only on an older engine; the TA1 branch ignores them.
173
+ if site.class_name in ("_PFAnchoredVWAP", "_PFAnchoredVWAPBands"):
174
+ explicit = [
175
+ self._ta_compute_arg_cpp(ta_name, i, a)
176
+ for i, a in enumerate(site.compute_args)
177
+ ]
178
+ if len(explicit) < 2:
179
+ raise AssertionError("anchored VWAP site is missing source/anchor")
180
+ return (
181
+ f"{explicit[0]}, current_bar_.volume, current_bar_.timestamp, "
182
+ f"syminfo_.timezone, syminfo_.session, {explicit[1]}"
183
+ )
184
+
185
+ if site.class_name == "_PFPivotPointLevels":
186
+ explicit = [
187
+ self._ta_compute_arg_cpp(ta_name, i, a)
188
+ for i, a in enumerate(site.compute_args)
189
+ ]
190
+ if len(explicit) < 3:
191
+ raise AssertionError("pivot TA1 site is missing anchor arguments")
192
+ return (
193
+ f"{explicit[0]}, {explicit[1]}, {explicit[2]}, "
194
+ "current_bar_.open, current_bar_.high, current_bar_.low, "
195
+ "current_bar_.close"
196
+ )
197
+
119
198
  if ta_name in TA_IMPLICIT_COMPUTE_FULL:
120
199
  implicit = TA_IMPLICIT_COMPUTE_FULL[ta_name]
121
200
  # issue #178: chart-context atr / tr take the previous CHART
@@ -123,14 +202,20 @@ class TaSiteHelper:
123
202
  if ta_name in TA_CHART_PREV_CLOSE:
124
203
  implicit = f"{implicit}, {TA_CHART_PREV_CLOSE_ARG}"
125
204
  if site.compute_args:
126
- explicit = ", ".join(self._visit_expr(a) for a in site.compute_args)
205
+ explicit = ", ".join(
206
+ self._ta_compute_arg_cpp(ta_name, i, a)
207
+ for i, a in enumerate(site.compute_args)
208
+ )
127
209
  if ta_name in self._TA_IMPLICIT_REPLACE:
128
210
  return explicit
129
211
  return f"{explicit}, {implicit}" if explicit else implicit
130
212
  return implicit
131
213
 
132
214
  if site.compute_args:
133
- explicit = ", ".join(self._visit_expr(a) for a in site.compute_args)
215
+ explicit = ", ".join(
216
+ self._ta_compute_arg_cpp(ta_name, i, a)
217
+ for i, a in enumerate(site.compute_args)
218
+ )
134
219
  if ta_name in TA_IMPLICIT_APPEND:
135
220
  return f"{explicit}, {TA_IMPLICIT_APPEND[ta_name]}"
136
221
  return explicit
@@ -392,48 +477,6 @@ class TaSiteHelper:
392
477
  "ycelestine77/quantbyboji exact on main, 2026-09-04)"
393
478
  )
394
479
 
395
- _LAZY_SOURCE_CLOCK_BAR_SOURCES = frozenset({
396
- "open", "high", "low", "close", "volume", "hl2", "hlc3", "ohlc4", "hlcc4",
397
- })
398
-
399
- def _script_has_user_binding(self, name: str) -> bool:
400
- cache = getattr(self, "_user_binding_names", None)
401
- if cache is None:
402
- cache = set()
403
- ast = getattr(self.ctx, "ast", None)
404
- for node in self._walk_ast(ast):
405
- if isinstance(node, VarDecl):
406
- cache.add(node.name)
407
- elif isinstance(node, TupleAssign):
408
- cache.update(node.names)
409
- elif isinstance(node, ForStmt):
410
- cache.add(node.var)
411
- elif isinstance(node, ForInStmt):
412
- if node.var:
413
- cache.add(node.var)
414
- cache.update(node.vars or ())
415
- elif isinstance(node, FuncDef):
416
- cache.add(node.name)
417
- cache.update(node.params)
418
- self._user_binding_names = cache
419
- return name in cache
420
-
421
- def _lazy_source_clock_chart_source(self, site: "TACallSite"):
422
- """The unshadowed bar-builtin source node, or None.
423
-
424
- Between two executions closer than ``length`` bars the pin does not
425
- distinguish the held read from #64's eager chart ``source[length]``;
426
- the eager read is kept where the source is a plain chart builtin.
427
- """
428
- source = site.compute_args[0] if site.compute_args else None
429
- if (
430
- isinstance(source, Identifier)
431
- and source.name in self._LAZY_SOURCE_CLOCK_BAR_SOURCES
432
- and not self._script_has_user_binding(source.name)
433
- ):
434
- return source
435
- return None
436
-
437
480
  @staticmethod
438
481
  def _lazy_source_clock_length_node(node):
439
482
  kwargs = getattr(node, "kwargs", None) or {}
@@ -503,16 +546,9 @@ class TaSiteHelper:
503
546
  clocks: dict[int, dict] = {}
504
547
  routed = self._lazy_edge_ta_hoist_plan()["source_clock"]
505
548
  for index, (node_id, info) in enumerate(routed.items(), start=1):
506
- chart_source = self._lazy_source_clock_chart_source(info["site"])
507
549
  clocks[node_id] = {
508
550
  "clock": allocate(f"_pf_lazy_src_clock_{index}", reserved_members),
509
551
  "hist": allocate(f"_pf_lazy_src_hist_{index}", reserved_members),
510
- "chart": (
511
- allocate(f"_pf_lazy_src_chart_{index}", reserved_members)
512
- if chart_source is not None
513
- else None
514
- ),
515
- "chart_source": chart_source,
516
552
  "site": info["site"],
517
553
  "node": info["node"],
518
554
  "length_literal": info["length_literal"],
@@ -525,22 +561,22 @@ class TaSiteHelper:
525
561
  source = self._visit_expr(site.compute_args[0])
526
562
  length_node = self._lazy_source_clock_length_node(node)
527
563
  literal = self._lazy_source_clock_length_literal(length_node)
528
- chart = info["chart"]
529
564
  if literal is not None:
530
565
  length_expr = str(literal)
531
566
  held = f"{info['hist']}[{literal - 1}]" if literal >= 1 else "na<double>()"
532
- eager = f"{chart}[{literal}]" if chart is not None else held
533
567
  else:
534
- length_expr = f"(int)({self._visit_expr(length_node)})"
568
+ length_raw = self._visit_expr(length_node)
569
+ length_expr = self._coerce_int_slot(
570
+ length_raw, length_node, "int"
571
+ )
572
+ if (length_expr == length_raw
573
+ and not self._emitted_value_is_double(length_node)):
574
+ length_expr = pine_index_int_cast(length_raw)
535
575
  held = (
536
576
  f"(({length_expr}) >= 1 ? {info['hist']}[({length_expr}) - 1] "
537
577
  f": na<double>())"
538
578
  )
539
- eager = f"{chart}[{length_expr}]" if chart is not None else held
540
- previous = (
541
- f"{info['clock']}.previous_source({held}, {eager}, {length_expr}, "
542
- f"bar_index_)"
543
- )
579
+ previous = f"{info['clock']}.previous_source({held}, {length_expr})"
544
580
  method = "roc" if self._ta_name_from_site(site) == "roc" else "change"
545
581
  return f"{info['clock']}.{method}({source}, {previous})"
546
582
 
@@ -558,10 +594,8 @@ class TaSiteHelper:
558
594
  "// history is na before the first execution. The paired hist Series",
559
595
  "// holds `bar_base_source` (the value committed by earlier bars) once",
560
596
  "// per chart bar, so `hist[length - 1]` is the source at the most",
561
- "// recent execution at or before bar-length. Between two executions",
562
- "// closer than `length` bars the tapes do not distinguish that read",
563
- "// from the eager chart `source[length]`, so the #64 eager read is",
564
- "// kept there for chart-builtin sources.",
597
+ "// recent execution at or before bar-length, also when the previous",
598
+ "// execution is closer than `length` bars.",
565
599
  f"struct {type_name} {{",
566
600
  " double committed_source = na<double>();",
567
601
  " int committed_bar = -1;",
@@ -587,12 +621,11 @@ class TaSiteHelper:
587
621
  " }",
588
622
  " }",
589
623
  "",
590
- " double previous_source(double held, double eager, int length,",
591
- " int bar) const {",
624
+ " double previous_source(double held, int length) const {",
592
625
  " if (bar_base_bar < 0 || length < 1) {",
593
626
  " return na<double>();",
594
627
  " }",
595
- " return bar - bar_base_bar >= length ? held : eager;",
628
+ " return held;",
596
629
  " }",
597
630
  "",
598
631
  " double change(double source, double previous) {",
@@ -651,6 +684,10 @@ class TaSiteHelper:
651
684
  and request.security sites retain their existing behavior."""
652
685
  if not getattr(site, "is_static", False):
653
686
  return False
687
+ if self._ta_dynamic_plan(site) is not None:
688
+ # The length is read on the bar (its first execution, or every
689
+ # call); precalculate() has no such value.
690
+ return False
654
691
  node = getattr(site, "node", None)
655
692
  plan = self._lazy_edge_ta_hoist_plan()
656
693
  if node is not None and (
@@ -867,29 +904,78 @@ class TaSiteHelper:
867
904
  return
868
905
  # Literals, identifiers and anything else are leaves.
869
906
 
870
- def roots(stmt) -> list:
871
- if isinstance(stmt, VarDecl):
872
- if stmt.is_var or stmt.is_varip:
873
- return []
874
- return [stmt.value]
875
- if isinstance(stmt, Assignment):
876
- return [stmt.target, stmt.value]
877
- if isinstance(stmt, TupleAssign):
878
- return [stmt.value]
879
- if isinstance(stmt, ExprStmt):
880
- return [stmt.expr]
881
- if isinstance(stmt, IfStmt):
882
- return [stmt.condition]
883
- return []
884
-
885
907
  ast = getattr(self.ctx, "ast", None)
886
908
  for stmt in getattr(ast, "body", ()) or ():
887
909
  units: list[dict] = []
888
- for root in roots(stmt):
910
+ for root in self._lazy_edge_statement_roots(stmt):
889
911
  scan(root, False, units)
890
912
  if units:
891
913
  by_stmt[id(stmt)] = units
892
914
 
915
+ # A change / mom / roc call in a top-level if block -- an else-if's
916
+ # and a nested if's too -- runs only on the bars its block runs, and
917
+ # reads its own held source history as a lazy operand's call does:
918
+ # TradingView's tape of te_lazy_if_source reads ta.roc(close, 3) on
919
+ # its tenth bar from the close its third bar held, where the
920
+ # executions' own ring is still short (and the stateful ROC oracles
921
+ # pf-probe-quant-stateful-roc-* read their reached ROC so). Loop and
922
+ # switch bodies, a var initializer and a call read with history keep
923
+ # their lowering.
924
+ def block_sites(node, history_read: bool = False) -> None:
925
+ if node is None or isinstance(node, (str, int, float, bool)):
926
+ return
927
+ if isinstance(node, (list, tuple)):
928
+ for child in node:
929
+ block_sites(child)
930
+ return
931
+ if not isinstance(node, ASTNode) or isinstance(
932
+ node, (FuncDef, MethodDef, ForStmt, ForInStmt, WhileStmt, SwitchStmt)):
933
+ return
934
+ if isinstance(node, VarDecl) and (node.is_var or node.is_varip):
935
+ return
936
+ if isinstance(node, Subscript):
937
+ block_sites(node.object, history_read=True)
938
+ block_sites(node.index)
939
+ return
940
+ if isinstance(node, FuncCall):
941
+ callee = node.callee
942
+ is_security = (
943
+ isinstance(callee, MemberAccess)
944
+ and isinstance(callee.object, Identifier)
945
+ and callee.object.name == "request"
946
+ and callee.member in ("security", "security_lower_tf")
947
+ )
948
+ site = None if history_read else self._get_ta_site(node)
949
+ if (site is not None
950
+ and self._ta_name_from_site(site) in self.LAZY_SOURCE_CLOCK_TA
951
+ and self._lazy_source_clock_eligible(site)):
952
+ source_clock.setdefault(id(node), {
953
+ "node": node,
954
+ "site": site,
955
+ "length_literal": self._lazy_source_clock_length_literal(
956
+ self._lazy_source_clock_length_node(node)
957
+ ),
958
+ })
959
+ for idx, arg in enumerate(getattr(node, "args", ()) or ()):
960
+ if not (is_security and idx == 2):
961
+ block_sites(arg)
962
+ for key, value in (getattr(node, "kwargs", None) or {}).items():
963
+ if not (is_security and key == "expression"):
964
+ block_sites(value)
965
+ return
966
+ for key, value in vars(node).items():
967
+ if key not in ("loc", "annotations"):
968
+ block_sites(value)
969
+
970
+ for stmt in getattr(ast, "body", ()) or ():
971
+ blocks = [stmt] if isinstance(stmt, IfStmt) else [
972
+ value for value in (getattr(stmt, "value", None),)
973
+ if isinstance(stmt, (VarDecl, Assignment)) and isinstance(value, IfStmt)
974
+ ]
975
+ for block in blocks:
976
+ block_sites(block.body)
977
+ block_sites(block.else_body)
978
+
893
979
  plan = {
894
980
  "by_stmt": by_stmt,
895
981
  "call_nodes": call_nodes,
@@ -934,6 +1020,214 @@ class TaSiteHelper:
934
1020
  self._hoisted_ta_values.clear()
935
1021
  self._hoisted_hist_reads.clear()
936
1022
 
1023
+ @staticmethod
1024
+ def _lazy_edge_statement_roots(stmt) -> list:
1025
+ """The expressions of one statement a scope runs on every execution:
1026
+ a ``VarDecl`` value (not a ``var``/``varip`` initializer), an
1027
+ ``Assignment``'s target and value, a ``TupleAssign`` value, an
1028
+ ``ExprStmt`` and an ``IfStmt``'s head condition."""
1029
+ if isinstance(stmt, VarDecl):
1030
+ if stmt.is_var or stmt.is_varip:
1031
+ return []
1032
+ return [stmt.value]
1033
+ if isinstance(stmt, Assignment):
1034
+ return [stmt.target, stmt.value]
1035
+ if isinstance(stmt, TupleAssign):
1036
+ return [stmt.value]
1037
+ if isinstance(stmt, ExprStmt):
1038
+ return [stmt.expr]
1039
+ if isinstance(stmt, IfStmt):
1040
+ return [stmt.condition]
1041
+ return []
1042
+
1043
+ # ------------------------------------------------------------------
1044
+ # A pure user call's history below a lazy edge
1045
+ # ------------------------------------------------------------------
1046
+ #
1047
+ # TradingView rule, pinned 2026-09-29 with ``lab tv`` on BINANCE:BTCUSDT
1048
+ # 15 (``tests/fixtures/lazy_call_history``): ``f(...)[k]`` on a user
1049
+ # function's call in the lazily evaluated right operand of ``and``/``or``
1050
+ # or in a ternary's arm reads the call's value k executions of its scope
1051
+ # ago -- k bars at the top level, k calls inside a function -- on all 95
1052
+ # every-third-bar reads of each shape, in a function and at the top level:
1053
+ # the call runs on every execution of its scope, and the lazy edge gates
1054
+ # only the read. So ``isNew(s) => inS(s) and not inS(s)[1]`` is true on
1055
+ # the first bar of every session. The call-local history the Subscript
1056
+ # lowering keeps otherwise (pushed only where the operand runs) reads the
1057
+ # previous time the operand ran: ``[1]`` three bars back on the tape's
1058
+ # reads, and ``isNew`` true once in a whole run.
1059
+ #
1060
+ # Only a call whose value cannot depend on when it runs is hoisted: a
1061
+ # user function whose body is one expression of its parameters, bar
1062
+ # fields, literals, ``timeframe.*``/``syminfo.*`` facts, operators, and
1063
+ # ``na``/``nz``/``time``/``time_close`` or such functions (the tape's
1064
+ # shapes), called positionally with arguments of literals, names and
1065
+ # operators. A stateful or side-effecting callee, a read in an ``if`` or
1066
+ # loop body, a read not below a lazy edge, one in the arguments of an
1067
+ # every-bar ``ta.*`` hoist (already pushed on every bar) and one in a
1068
+ # statement the lowering drops keep their lowering.
1069
+
1070
+ _LAZY_CALL_HISTORY_BUILTINS = frozenset({"na", "nz", "time", "time_close"})
1071
+ _LAZY_CALL_HISTORY_NAMESPACES = frozenset({"timeframe", "syminfo"})
1072
+
1073
+ def _lazy_call_history_pure_expr(self, expr, params: set[str] | None) -> bool:
1074
+ """Whether ``expr`` reads only literals, names and fixed facts through
1075
+ operators -- and, when ``params`` is given (a function body), bar
1076
+ fields, parameters and the calls ``_lazy_call_history_pure_call``
1077
+ admits instead of arbitrary names."""
1078
+ stack = [expr]
1079
+ while stack:
1080
+ n = stack.pop()
1081
+ if isinstance(n, (NumberLiteral, BoolLiteral, StringLiteral, NaLiteral,
1082
+ ColorLiteral)):
1083
+ continue
1084
+ if isinstance(n, Identifier):
1085
+ if (params is None or n.name in params or n.name in BAR_SERIES_PUSH
1086
+ or n.name in ("bar_index", "time")):
1087
+ continue
1088
+ return False
1089
+ if isinstance(n, MemberAccess):
1090
+ if (isinstance(n.object, Identifier)
1091
+ and n.object.name in self._LAZY_CALL_HISTORY_NAMESPACES):
1092
+ continue
1093
+ return False
1094
+ if isinstance(n, BinOp):
1095
+ stack.extend((n.left, n.right))
1096
+ elif isinstance(n, UnaryOp):
1097
+ stack.append(n.operand)
1098
+ elif isinstance(n, Ternary):
1099
+ stack.extend((n.condition, n.true_val, n.false_val))
1100
+ elif (params is not None and isinstance(n, FuncCall)
1101
+ and isinstance(n.callee, Identifier) and not n.kwargs
1102
+ and (n.callee.name in self._LAZY_CALL_HISTORY_BUILTINS
1103
+ or self._lazy_call_history_pure_call(n))):
1104
+ stack.extend(n.args)
1105
+ else:
1106
+ return False
1107
+ return True
1108
+
1109
+ def _lazy_call_history_pure_call(self, call) -> bool:
1110
+ """A positional call of the one user function of its name whose body
1111
+ is a single ``_lazy_call_history_pure_expr`` of its parameters. A body
1112
+ reached again through its own calls is not pure."""
1113
+ if not isinstance(call, FuncCall) or not isinstance(call.callee, Identifier):
1114
+ return False
1115
+ name = call.callee.name
1116
+ overloads = [fi for fi in self.ctx.func_infos if fi.name == name]
1117
+ if call.kwargs or len(overloads) != 1 or overloads[0].node is None:
1118
+ return False
1119
+ node = overloads[0].node
1120
+ if len(call.args) != len(node.params):
1121
+ return False
1122
+ cache = getattr(self, "_lazy_call_history_pure_cache", None)
1123
+ if cache is None:
1124
+ cache = self._lazy_call_history_pure_cache = {}
1125
+ if id(node) not in cache:
1126
+ cache[id(node)] = False
1127
+ body = getattr(node, "body", None) or []
1128
+ cache[id(node)] = (
1129
+ len(body) == 1 and isinstance(body[0], ExprStmt)
1130
+ and self._lazy_call_history_pure_expr(body[0].expr, set(node.params))
1131
+ )
1132
+ return cache[id(node)]
1133
+
1134
+ def _lazy_call_history_units(self, stmt) -> list:
1135
+ """The ``pure_call(...)[k]`` reads below a lazy edge of ``stmt``'s
1136
+ roots (``_lazy_edge_statement_roots``), in evaluation order, whose
1137
+ arguments are ``_lazy_call_history_pure_expr``. A
1138
+ ``request.security*`` payload, which its own evaluator runs, is not
1139
+ walked."""
1140
+ cache = getattr(self, "_lazy_call_history_units_cache", None)
1141
+ if cache is None:
1142
+ cache = self._lazy_call_history_units_cache = {}
1143
+ if id(stmt) in cache:
1144
+ return cache[id(stmt)]
1145
+ units: list = []
1146
+ # A statement the lowering drops (plot, bgcolor, ...) reads nothing.
1147
+ if isinstance(stmt, ExprStmt) and self._is_skip_expr(stmt.expr):
1148
+ cache[id(stmt)] = units
1149
+ return units
1150
+ hoisted_ta = self._lazy_edge_hoisted_ta_call_nodes()
1151
+
1152
+ def scan(expr, under_lazy: bool) -> None:
1153
+ if expr is None:
1154
+ return
1155
+ if isinstance(expr, Subscript):
1156
+ call = expr.object
1157
+ if (under_lazy and self._lazy_call_history_pure_call(call)
1158
+ and all(self._lazy_call_history_pure_expr(arg, None)
1159
+ for arg in call.args)):
1160
+ units.append(expr)
1161
+ return
1162
+ scan(expr.object, under_lazy)
1163
+ scan(expr.index, under_lazy)
1164
+ elif isinstance(expr, FuncCall):
1165
+ # An every-bar ta.* hoist evaluates its arguments on every
1166
+ # bar, so a read inside one already pushes on every bar.
1167
+ if id(expr) in hoisted_ta:
1168
+ return
1169
+ callee = expr.callee
1170
+ is_security = (
1171
+ isinstance(callee, MemberAccess)
1172
+ and isinstance(callee.object, Identifier)
1173
+ and callee.object.name == "request"
1174
+ and callee.member in ("security", "security_lower_tf")
1175
+ )
1176
+ scan(callee, under_lazy)
1177
+ for idx, arg in enumerate(getattr(expr, "args", ()) or ()):
1178
+ if not (is_security and idx == 2):
1179
+ scan(arg, under_lazy)
1180
+ for key, value in (getattr(expr, "kwargs", None) or {}).items():
1181
+ if not (is_security and key == "expression"):
1182
+ scan(value, under_lazy)
1183
+ elif isinstance(expr, BinOp):
1184
+ scan(expr.left, under_lazy)
1185
+ scan(expr.right, under_lazy or expr.op in ("and", "or"))
1186
+ elif isinstance(expr, Ternary):
1187
+ scan(expr.condition, under_lazy)
1188
+ scan(expr.true_val, True)
1189
+ scan(expr.false_val, True)
1190
+ elif isinstance(expr, UnaryOp):
1191
+ scan(expr.operand, under_lazy)
1192
+ elif isinstance(expr, MemberAccess):
1193
+ scan(expr.object, under_lazy)
1194
+ elif isinstance(expr, TupleLiteral):
1195
+ for element in expr.elements:
1196
+ scan(element, under_lazy)
1197
+ elif isinstance(expr, IfStmt):
1198
+ scan(expr.condition, under_lazy)
1199
+ elif isinstance(expr, SwitchStmt):
1200
+ scan(expr.expr, under_lazy)
1201
+
1202
+ for root in self._lazy_edge_statement_roots(stmt):
1203
+ scan(root, False)
1204
+ cache[id(stmt)] = units
1205
+ return units
1206
+
1207
+ def _emit_lazy_call_history_hoists(self, stmt, lines: list[str], indent: int) -> list[int]:
1208
+ """Push each ``_lazy_call_history_units`` read's call once, before
1209
+ ``stmt``, and make the read return its Series. Returns the reads to
1210
+ hand ``_clear_lazy_call_history_hoists`` once ``stmt`` is lowered."""
1211
+ units = self._lazy_call_history_units(stmt)
1212
+ if not units:
1213
+ return []
1214
+ pad = " " * indent
1215
+ lines.append(
1216
+ f"{pad}// Pine v6 lazy operand: a call read at an offset runs on every "
1217
+ "execution, only the read is gated."
1218
+ )
1219
+ hoisted = []
1220
+ for read in units:
1221
+ member = self._inline_history_member("hist_call", read)
1222
+ self._emit_history_series_write(lines, pad, member, self._visit_expr(read.object))
1223
+ self._hoisted_hist_reads[id(read)] = member
1224
+ hoisted.append(id(read))
1225
+ return hoisted
1226
+
1227
+ def _clear_lazy_call_history_hoists(self, hoisted: list[int]) -> None:
1228
+ for key in hoisted:
1229
+ self._hoisted_hist_reads.pop(key, None)
1230
+
937
1231
  def _security_ta_compute_args_for_site(
938
1232
  self,
939
1233
  sec_id: int,
@@ -942,6 +1236,38 @@ class TaSiteHelper:
942
1236
  security_mutable_names: set[str] | None = None,
943
1237
  helper_binding_stack: tuple[dict, ...] | None = None,
944
1238
  emitted_lines: list[str] | None = None,
1239
+ ) -> str:
1240
+ """``_security_ta_compute_args_for_site_base`` plus the per-call
1241
+ arguments of a copy whose length the constructor cannot take
1242
+ (``_ta_security_plan``), a series argument rendered in the requested
1243
+ context."""
1244
+ args = self._security_ta_compute_args_for_site_base(
1245
+ sec_id, site, ta_results, security_mutable_names,
1246
+ helper_binding_stack, emitted_lines,
1247
+ )
1248
+ plan = self._ta_security_plan(sec_id, site, helper_binding_stack)
1249
+ if plan is None:
1250
+ return args
1251
+ return self._ta_dynamic_compute_args(
1252
+ site,
1253
+ plan,
1254
+ args,
1255
+ lambda node: self._build_security_expr(
1256
+ sec_id, node, None, ta_results,
1257
+ security_mutable_names=security_mutable_names,
1258
+ helper_binding_stack=helper_binding_stack,
1259
+ emitted_lines=emitted_lines,
1260
+ ),
1261
+ )
1262
+
1263
+ def _security_ta_compute_args_for_site_base(
1264
+ self,
1265
+ sec_id: int,
1266
+ site: "TACallSite",
1267
+ ta_results: dict[int, str],
1268
+ security_mutable_names: set[str] | None = None,
1269
+ helper_binding_stack: tuple[dict, ...] | None = None,
1270
+ emitted_lines: list[str] | None = None,
945
1271
  ) -> str:
946
1272
  """Same as ``_ta_compute_args_for_site`` but inside an ``evaluate_security`` body.
947
1273
 
@@ -951,20 +1277,87 @@ class TaSiteHelper:
951
1277
  mutable globals get rebound to the security-context shadows."""
952
1278
  ta_name = self._ta_name_from_site(site)
953
1279
 
954
- if ta_name in TA_IMPLICIT_COMPUTE_FULL:
955
- implicit = TA_IMPLICIT_COMPUTE_FULL[ta_name].replace("current_bar_.", "bar.")
956
- if site.compute_args:
957
- explicit = ", ".join(
1280
+ if site.class_name in ("_PFAnchoredVWAP", "_PFAnchoredVWAPBands"):
1281
+ explicit = [
1282
+ self._coerce_bool_expr(
958
1283
  self._build_security_expr(
959
- sec_id,
960
- a,
961
- None,
962
- ta_results,
1284
+ sec_id, a, None, ta_results,
1285
+ security_mutable_names=security_mutable_names,
1286
+ helper_binding_stack=helper_binding_stack,
1287
+ emitted_lines=emitted_lines,
1288
+ ),
1289
+ a,
1290
+ ) if i in self._TA_BOOL_COMPUTE_ARGS.get(ta_name, ()) else
1291
+ self._build_security_expr(
1292
+ sec_id, a, None, ta_results,
1293
+ security_mutable_names=security_mutable_names,
1294
+ helper_binding_stack=helper_binding_stack,
1295
+ emitted_lines=emitted_lines,
1296
+ )
1297
+ for i, a in enumerate(site.compute_args)
1298
+ ]
1299
+ if len(explicit) < 2:
1300
+ raise AssertionError("anchored VWAP security site is missing source/anchor")
1301
+ return (
1302
+ f"{explicit[0]}, bar.volume, bar.timestamp, syminfo_.timezone, "
1303
+ f"syminfo_.session, {explicit[1]}"
1304
+ )
1305
+
1306
+ if site.class_name == "_PFPivotPointLevels":
1307
+ explicit = [
1308
+ self._coerce_bool_expr(
1309
+ self._build_security_expr(
1310
+ sec_id, a, None, ta_results,
963
1311
  security_mutable_names=security_mutable_names,
964
1312
  helper_binding_stack=helper_binding_stack,
965
1313
  emitted_lines=emitted_lines,
1314
+ ),
1315
+ a,
1316
+ ) if i in self._TA_BOOL_COMPUTE_ARGS.get(ta_name, ()) else
1317
+ self._build_security_expr(
1318
+ sec_id, a, None, ta_results,
1319
+ security_mutable_names=security_mutable_names,
1320
+ helper_binding_stack=helper_binding_stack,
1321
+ emitted_lines=emitted_lines,
1322
+ )
1323
+ for i, a in enumerate(site.compute_args)
1324
+ ]
1325
+ if len(explicit) < 3:
1326
+ raise AssertionError("pivot security site is missing anchor arguments")
1327
+ return (
1328
+ f"{explicit[0]}, {explicit[1]}, {explicit[2]}, bar.open, bar.high, "
1329
+ "bar.low, bar.close"
1330
+ )
1331
+
1332
+ if ta_name in TA_IMPLICIT_COMPUTE_FULL:
1333
+ implicit = TA_IMPLICIT_COMPUTE_FULL[ta_name].replace("current_bar_.", "bar.")
1334
+ if site.compute_args:
1335
+ explicit = ", ".join(
1336
+ (
1337
+ self._coerce_bool_expr(
1338
+ self._build_security_expr(
1339
+ sec_id,
1340
+ a,
1341
+ None,
1342
+ ta_results,
1343
+ security_mutable_names=security_mutable_names,
1344
+ helper_binding_stack=helper_binding_stack,
1345
+ emitted_lines=emitted_lines,
1346
+ ),
1347
+ a,
1348
+ )
1349
+ if i in self._TA_BOOL_COMPUTE_ARGS.get(ta_name, ())
1350
+ else self._build_security_expr(
1351
+ sec_id,
1352
+ a,
1353
+ None,
1354
+ ta_results,
1355
+ security_mutable_names=security_mutable_names,
1356
+ helper_binding_stack=helper_binding_stack,
1357
+ emitted_lines=emitted_lines,
1358
+ )
966
1359
  )
967
- for a in site.compute_args
1360
+ for i, a in enumerate(site.compute_args)
968
1361
  )
969
1362
  if ta_name in self._TA_IMPLICIT_REPLACE:
970
1363
  return explicit
@@ -973,16 +1366,31 @@ class TaSiteHelper:
973
1366
 
974
1367
  if site.compute_args:
975
1368
  explicit = ", ".join(
976
- self._build_security_expr(
977
- sec_id,
978
- a,
979
- None,
980
- ta_results,
981
- security_mutable_names=security_mutable_names,
982
- helper_binding_stack=helper_binding_stack,
983
- emitted_lines=emitted_lines,
1369
+ (
1370
+ self._coerce_bool_expr(
1371
+ self._build_security_expr(
1372
+ sec_id,
1373
+ a,
1374
+ None,
1375
+ ta_results,
1376
+ security_mutable_names=security_mutable_names,
1377
+ helper_binding_stack=helper_binding_stack,
1378
+ emitted_lines=emitted_lines,
1379
+ ),
1380
+ a,
1381
+ )
1382
+ if i in self._TA_BOOL_COMPUTE_ARGS.get(ta_name, ())
1383
+ else self._build_security_expr(
1384
+ sec_id,
1385
+ a,
1386
+ None,
1387
+ ta_results,
1388
+ security_mutable_names=security_mutable_names,
1389
+ helper_binding_stack=helper_binding_stack,
1390
+ emitted_lines=emitted_lines,
1391
+ )
984
1392
  )
985
- for a in site.compute_args
1393
+ for i, a in enumerate(site.compute_args)
986
1394
  )
987
1395
  if ta_name in TA_IMPLICIT_APPEND:
988
1396
  implicit = TA_IMPLICIT_APPEND[ta_name].replace("current_bar_.", "bar.")
@@ -994,6 +1402,686 @@ class TaSiteHelper:
994
1402
 
995
1403
  return ""
996
1404
 
1405
+ # ------------------------------------------------------------------
1406
+ # Lengths the constructor cannot take (K-TA-DYNLEN)
1407
+ # ------------------------------------------------------------------
1408
+ #
1409
+ # A ``ta.*`` constructor argument that is neither a compile-time value nor
1410
+ # a runtime-resettable input/timeframe expression used to be refused. Its
1411
+ # Pine qualifier decides TradingView's answer (lab tv, K-TA-DYNLEN
1412
+ # 2026-09-26; the engine header pineforge/source/pine_ta_length.hpp states
1413
+ # every rule with its tape):
1414
+ #
1415
+ # * simple (fixed for the run -- a syminfo preset, str.* over one):
1416
+ # TradingView answers exactly the constant-length call
1417
+ # (the ring of a sparse ta.lowest included). Lowered onto
1418
+ # ``pineforge::source::FirstCallBound<class>``, built from the call's
1419
+ # first execution; a length goes through ``simple_ta_length`` (0, a
1420
+ # negative length or na stops the run, as TradingView does).
1421
+ # * series, for ta.highest / ta.lowest / ta.highestbars / ta.lowestbars:
1422
+ # re-windowed on every call -- ``pineforge::source::Series*``, the length
1423
+ # passed per call.
1424
+ # * ta.supertrend: the factor (series or simple) of the first execution is
1425
+ # the run's -- ``pineforge::source::PineSupertrend``.
1426
+ #
1427
+ # TradingView refuses a series length for ta.rma / ema / rsi / atr / dmi /
1428
+ # macd / kc / hma / tsi / rci and supertrend's atrPeriod at compile time,
1429
+ # so those only ever carry a simple one here. A series length for the
1430
+ # other window functions (ta.sma, ta.wma, ...) keeps the refusal.
1431
+ # Constant- and input-length sites never reach this path: their C++ is
1432
+ # unchanged. An input.string choice of a length (``mode == "A" ? 5 : 10``)
1433
+ # is an input length since CG-SECURITY-2 spells string literals; the other
1434
+ # arguments those literals bring to the constructor stay here
1435
+ # (``_ta_arg_takes_plan``).
1436
+
1437
+ _TA_SERIES_EXTREME_CLASSES = {
1438
+ "highest": "pineforge::source::SeriesHighest",
1439
+ "lowest": "pineforge::source::SeriesLowest",
1440
+ "highestbars": "pineforge::source::SeriesHighestBars",
1441
+ "lowestbars": "pineforge::source::SeriesLowestBars",
1442
+ }
1443
+ # Pure str.* functions of simple arguments (a simple result).
1444
+ _SIMPLE_STR_FUNCS = frozenset({
1445
+ "upper", "lower", "contains", "startswith", "endswith", "length",
1446
+ "pos", "substring", "replace", "replace_all", "trim", "repeat",
1447
+ "tonumber", "tostring",
1448
+ })
1449
+
1450
+ def _ta_site_function(self, site: "TACallSite") -> str:
1451
+ """The Pine ``ta.<name>`` of a site (a clone keeps its template's node)."""
1452
+ node = getattr(site, "node", None)
1453
+ if isinstance(node, FuncCall):
1454
+ func_name, namespace = self._resolve_callee(node.callee)
1455
+ if namespace == "ta" and func_name:
1456
+ return func_name
1457
+ return self._ta_name_from_site(site)
1458
+
1459
+ def _ta_dynamic_plan(self, site: "TACallSite") -> dict | None:
1460
+ """The lowering of a site with a constructor argument the constructor
1461
+ cannot take, or None (the ordinary constructor / runtime-reset path).
1462
+ ``kind`` is ``first_call`` / ``series_extreme`` / ``supertrend``, or
1463
+ ``refused`` (the constructor guard reports it)."""
1464
+ cache = self.__dict__.setdefault("_ta_dynamic_plan_cache", {})
1465
+ key = id(site)
1466
+ if key not in cache:
1467
+ cache[key] = self._build_ta_dynamic_plan(site)
1468
+ plan = cache[key]
1469
+ if plan is None or plan["kind"] == "refused":
1470
+ return None
1471
+ return plan
1472
+
1473
+ def _ta_dynamic_refusal(self, site: "TACallSite") -> dict | None:
1474
+ """The refused plan of a site, when it has one."""
1475
+ self._ta_dynamic_plan(site)
1476
+ plan = self.__dict__.get("_ta_dynamic_plan_cache", {}).get(id(site))
1477
+ if plan is not None and plan["kind"] == "refused":
1478
+ return plan
1479
+ return None
1480
+
1481
+ # A string literal in a length's spelling, and the names a length may read
1482
+ # beside input-backed ones and still be a choice between inputs' options.
1483
+ _TA_STRING_LITERAL = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
1484
+ _TA_INPUT_CHOICE_NAMES = frozenset({
1485
+ "and", "or", "not", "true", "false", "na", "int", "float", "bool",
1486
+ "string", "input", "math",
1487
+ })
1488
+
1489
+ def _ta_length_literal_facts(self, arg: str, inline: bool = True) -> tuple[bool, bool]:
1490
+ """Whether the constructor argument ``arg`` reaches the constructor
1491
+ only because string literals are spelled -- a derived name whose value
1492
+ holds a literal (it went untracked), or, when ``inline``, a literal
1493
+ naming something written in ``arg`` itself (the reset path's
1494
+ identifier scans read it as a name) -- and whether it then reads
1495
+ anything beside inputs, literals and ``math.*``.
1496
+
1497
+ CG-SECURITY-2 spells those literals so that a length chosen by
1498
+ comparing an ``input.string`` with its options is input-derived and
1499
+ keeps its constructor. Any other argument they bring to the
1500
+ constructor -- a ``syminfo.*`` or ``timeframe.*`` comparison,
1501
+ ``str.*`` over one -- stays with this module's lowering, as before
1502
+ (the reset path reads ``timeframe.*`` on the chart's timeframe in a
1503
+ request.security copy, where TradingView reads the requested one)."""
1504
+ newly = False
1505
+ names: set[str] = set()
1506
+ pending, seen = [(arg, inline)], set()
1507
+ while pending:
1508
+ raw, literal_counts = pending.pop()
1509
+ text = self._inline_inputs_masked(raw)
1510
+ if literal_counts and any(re.search(r"[A-Za-z_]", lit[1:-1])
1511
+ for lit in self._TA_STRING_LITERAL.findall(text)):
1512
+ newly = True
1513
+ for name in re.findall(r"[A-Za-z_][A-Za-z_0-9]*", blank_string_literals(text)):
1514
+ derived = self._derived_input_expr.get(name)
1515
+ if derived is None or self._known_var_is_lexically_shadowed(name):
1516
+ names.add(name)
1517
+ elif name not in seen:
1518
+ seen.add(name)
1519
+ if self._TA_STRING_LITERAL.search(self._inline_inputs_masked(derived)):
1520
+ newly = True
1521
+ pending.append((derived, True))
1522
+ if not newly:
1523
+ return False, False
1524
+ allowed = (self._TA_INPUT_CHOICE_NAMES | self._input_backed_vars
1525
+ | set(self._MATH_MEMBER_CPP))
1526
+ outside = {
1527
+ name for name in names - allowed
1528
+ if name not in self._known_vars or self._known_var_is_lexically_shadowed(name)
1529
+ }
1530
+ return True, bool(outside)
1531
+
1532
+ def _ta_arg_takes_plan(self, site: "TACallSite", position: int, arg: str,
1533
+ inline: bool = True) -> bool:
1534
+ """A constructor argument the constructor path takes only because
1535
+ string literals are spelled, that this module lowers instead
1536
+ (``_ta_length_literal_facts``): anything beside an input.string
1537
+ choice, and an input.string choice of a parameter that is not known
1538
+ to be an int -- a float, or one ``_ta_ctor_param`` does not know (a
1539
+ VWAP band multiplier, ``math.sum``'s length) -- since the runtime
1540
+ reset casts its expression to a length."""
1541
+ newly, outside = self._ta_length_literal_facts(arg, inline)
1542
+ if not newly:
1543
+ return False
1544
+ if outside:
1545
+ return True
1546
+ param = self._ta_ctor_param(site, position)
1547
+ return param is None or param.pine_type != PineType.INT
1548
+
1549
+ def _ta_bound_arg_takes_plan(self, site: "TACallSite", position: int, node) -> bool:
1550
+ """``_ta_arg_takes_plan`` of a helper-bound argument's AST. A literal
1551
+ written in the helper call itself always reached the constructor
1552
+ (its AST is stable); only a derived name's counts."""
1553
+ spelled = self._arith_expr_to_str(node) if node is not None else None
1554
+ return spelled is not None and self._ta_arg_takes_plan(site, position, spelled, inline=False)
1555
+
1556
+ def _build_ta_dynamic_plan(self, site: "TACallSite") -> dict | None:
1557
+ if not site.ctor_args:
1558
+ return None
1559
+ refused = [
1560
+ pos for pos, arg in enumerate(site.ctor_args)
1561
+ if not self._is_compile_time_value(self._resolve_ta_ctor_arg(arg))
1562
+ and (self._runtime_ctor_arg_for_reset(arg) is None
1563
+ or self._ta_arg_takes_plan(site, pos, arg))
1564
+ ]
1565
+ if not refused:
1566
+ return None
1567
+ nodes = list(getattr(site, "ctor_nodes", None) or [])
1568
+ if len(nodes) != len(site.ctor_args):
1569
+ return {"kind": "refused", "refused": refused}
1570
+ # A call outside every callable is spelled from its own nodes, whose
1571
+ # identifiers carry the analyzer's scope. A callable's constructor
1572
+ # arguments are its call site's spelling substituted into the body.
1573
+ own_nodes = site.owner_func is None
1574
+ simple = {
1575
+ pos: (self._simple_ta_arg_cpp_node(nodes[pos], scoped=True)
1576
+ if own_nodes else self._simple_ta_arg_cpp(site.ctor_args[pos]))
1577
+ for pos in refused
1578
+ }
1579
+ return self._ta_plan_for(site, refused, simple)
1580
+
1581
+ def _ta_plan_for(self, site: "TACallSite", refused: list, simple: dict) -> dict:
1582
+ """The plan of a site whose ``refused`` constructor arguments have the
1583
+ context-free C++ ``simple`` (None: series)."""
1584
+ family = self._ta_site_function(site)
1585
+ if family in self._TA_SERIES_EXTREME_CLASSES:
1586
+ kind = "first_call" if simple[refused[0]] is not None else "series_extreme"
1587
+ elif family == "supertrend":
1588
+ kind = "supertrend"
1589
+ elif all(simple[pos] is not None for pos in refused):
1590
+ kind = "first_call"
1591
+ else:
1592
+ return {"kind": "refused", "refused": refused}
1593
+ return {"kind": kind, "refused": refused, "simple": simple, "family": family}
1594
+
1595
+ def _ta_security_plan(self, sec_id: int, site: "TACallSite",
1596
+ helper_binding_stack=None) -> dict | None:
1597
+ """The lowering of the request.security copy of ``site`` that
1598
+ ``sec_id`` evaluates through ``helper_binding_stack`` (the variant
1599
+ key), or None (the constructor / runtime-reset path, whose guards
1600
+ report a length they cannot take). A copy reached through a helper
1601
+ call is planned from the arguments that call binds; a copy of a
1602
+ callable's request.security cloned per call site, from that call
1603
+ site's clone of the TA site."""
1604
+ stack = tuple(helper_binding_stack or ())
1605
+ key = (sec_id, id(site), self._security_binding_stack_signature(stack))
1606
+ cache = self.__dict__.setdefault("_ta_security_plan_cache", {})
1607
+ if key not in cache:
1608
+ cache[key] = self._build_ta_security_plan(sec_id, site, stack)
1609
+ return cache[key]
1610
+
1611
+ def _build_ta_security_plan(self, sec_id: int, site: "TACallSite",
1612
+ stack: tuple) -> dict | None:
1613
+ if not stack:
1614
+ ctor_site = self._security_ta_ctor_site(sec_id, site)
1615
+ plan = self._ta_dynamic_plan(ctor_site)
1616
+ if (plan is None and ctor_site is not site
1617
+ and self._ta_dynamic_plan(site) is not None):
1618
+ # The constructor path sizes every copy from the first call
1619
+ # site's arguments, which are not this call site's: build
1620
+ # the constant-length class from this call site's.
1621
+ plan = {"kind": "first_call", "refused": [], "simple": {},
1622
+ "family": self._ta_site_function(ctor_site)}
1623
+ if plan is None:
1624
+ # An input.string choice keeps the constructor, which sizes a
1625
+ # callable's one evaluator from its first call site's
1626
+ # arguments: refuse call sites passing different ones, as the
1627
+ # lowering above does (``_ta_length_literal_facts``).
1628
+ dead = getattr(self, "_dead_ta_indices", set())
1629
+ if any(
1630
+ self._ta_length_literal_facts(arg)[0]
1631
+ for index, other in enumerate(self.ctx.ta_call_sites)
1632
+ if other.node is site.node and index not in dead
1633
+ for arg in other.ctor_args
1634
+ ):
1635
+ self._refuse_shared_security_ta_lengths(sec_id, site)
1636
+ return None
1637
+ self._refuse_shared_security_ta_lengths(sec_id, site)
1638
+ return {**plan, "site": ctor_site, "sec_id": sec_id}
1639
+ if not site.ctor_args:
1640
+ return None
1641
+ nodes = self._security_ta_ctor_arg_nodes(site)
1642
+ ctor_args, stability = self._security_ta_ctor_args_for_variant(sec_id, site, stack)
1643
+ if stability is None or len(nodes) != len(ctor_args):
1644
+ if self._ta_dynamic_plan(site) is not None:
1645
+ self._codegen_error(
1646
+ site.node,
1647
+ f"Unsupported requested-context TA constructor for "
1648
+ f"{site.class_name}: PineForge cannot bind the helper "
1649
+ "call's arguments to its constructor arguments.",
1650
+ hint="Call the ta.* function directly inside request.security().",
1651
+ )
1652
+ return None
1653
+ bound = [self._security_helper_bound_ast(node, stack) for node in nodes]
1654
+ # The positions the runtime-reset path would refuse
1655
+ # (``_collect_ta_runtime_resets``); none -> that path, unchanged.
1656
+ # ``ctor_args`` are rendered C++ here: the bound argument's Pine
1657
+ # spelling decides ``_ta_bound_arg_takes_plan``.
1658
+ refused = [
1659
+ pos for pos, arg in enumerate(ctor_args)
1660
+ if not self._is_compile_time_value(self._resolve_ta_ctor_arg(arg))
1661
+ and ((self._runtime_ctor_arg_for_reset(arg) is None and not stability[pos])
1662
+ or self._ta_bound_arg_takes_plan(site, pos, bound[pos]))
1663
+ ]
1664
+ if not refused:
1665
+ return None
1666
+ bound_simple = {
1667
+ pos: self._simple_ta_arg_cpp_node(node, scoped=True)
1668
+ for pos, node in enumerate(bound) if node is not None
1669
+ }
1670
+ plan = self._ta_plan_for(
1671
+ site, refused, {pos: bound_simple.get(pos) for pos in refused})
1672
+ if plan["kind"] == "refused":
1673
+ forced = [pos for pos in plan["refused"]
1674
+ if stability[pos] or self._runtime_ctor_arg_for_reset(ctor_args[pos]) is not None]
1675
+ if forced:
1676
+ # An argument the constructor path takes only because string
1677
+ # literals are spelled (``_ta_bound_arg_takes_plan``) that this
1678
+ # lowering cannot spell either: refused, as before.
1679
+ pos = forced[0]
1680
+ self._refuse_security_ta_ctor_arg(site, pos, ctor_args[pos], nodes[pos], stack)
1681
+ return None
1682
+ return {**plan, "site": site, "sec_id": sec_id, "bound_simple": bound_simple}
1683
+
1684
+ def _refuse_security_ta_ctor_arg(self, site: "TACallSite", pos: int, cpp: str,
1685
+ node, stack) -> None:
1686
+ """Refuse a requested-context TA constructor argument the evaluator
1687
+ cannot size the object from, naming it. One that reads a helper's
1688
+ ``var`` (or a local an if arm or a history read makes a value of each
1689
+ requested bar) is named in Pine, with the local."""
1690
+ shown, reason = cpp, ""
1691
+ state = self._security_helper_state_reads(node, stack) if node is not None else []
1692
+ if state:
1693
+ shown = self._arith_expr_to_str(node) or cpp
1694
+ reason = (
1695
+ f": it reads {', '.join(repr(name) for name in state)}, a helper "
1696
+ "local holding a value of each requested bar, and PineForge takes "
1697
+ "such a length only for ta.highest, ta.lowest, ta.highestbars and "
1698
+ "ta.lowestbars"
1699
+ )
1700
+ self._codegen_error(
1701
+ site.node,
1702
+ f"Unsupported requested-context TA constructor "
1703
+ f"{'flag' if self._ta_ctor_arg_is_bool(site, pos) else 'length'} "
1704
+ f"'{shown}' for {site.class_name}: the "
1705
+ f"helper-bound expression is not a stable per-run scalar{reason}.",
1706
+ hint=("Use a literal, an input.*() value, timeframe.* metadata, "
1707
+ "or arithmetic over those for TA lengths."),
1708
+ )
1709
+
1710
+ def _security_call_item(self, sec_id: int) -> dict:
1711
+ for item in self._security_calls:
1712
+ if item["sec_id"] == sec_id:
1713
+ return item
1714
+ return {}
1715
+
1716
+ def _security_ta_ctor_site(self, sec_id: int, site: "TACallSite") -> "TACallSite":
1717
+ """The site whose constructor arguments the ``sec_id`` copy of
1718
+ ``site`` takes: a callable's request.security cloned per call site
1719
+ uses that call site's clone (``_collect_ta_runtime_resets`` does the
1720
+ same)."""
1721
+ item = self._security_call_item(sec_id)
1722
+ containing = item.get("containing_func") or ""
1723
+ cs_idx = item.get("callsite_idx")
1724
+ if containing and cs_idx is not None:
1725
+ remap = self._func_cs_ta_remap.get((containing, cs_idx)) or {}
1726
+ name = remap.get(site.member_name)
1727
+ if name and name != site.member_name:
1728
+ return self._ta_site_by_member().get(name, site)
1729
+ return site
1730
+
1731
+ def _refuse_shared_security_ta_lengths(self, sec_id: int, site: "TACallSite") -> None:
1732
+ """A callable's request.security that is not cloned per call site is
1733
+ one evaluator for every call: it can only take a length all the
1734
+ calls spell the same."""
1735
+ item = self._security_call_item(sec_id)
1736
+ containing = item.get("containing_func") or ""
1737
+ if not containing or item.get("callsite_idx") is not None:
1738
+ return
1739
+ dead = getattr(self, "_dead_ta_indices", set())
1740
+ spellings = sorted({
1741
+ ", ".join(other.ctor_args)
1742
+ for index, other in enumerate(self.ctx.ta_call_sites)
1743
+ if other.node is site.node and index not in dead
1744
+ })
1745
+ if len(spellings) > 1:
1746
+ self._codegen_error(
1747
+ site.node,
1748
+ f"Unsupported TA constructor length for {site.class_name} "
1749
+ f"inside request.security in '{containing}': its call sites "
1750
+ f"pass different lengths ({' / '.join(spellings)}), and one "
1751
+ "requested-context evaluator serves them all.",
1752
+ hint=("Call request.security() at each call site, or pass "
1753
+ "the same length from every call."),
1754
+ )
1755
+
1756
+ _TF_EXPR_ATOM = re.compile(r'"[^"\\]*"|\w+')
1757
+
1758
+ def _ta_payload_cpp(self, cpp: str, sec_id: int) -> str:
1759
+ """``cpp`` (a length rendered for the chart) evaluated in the
1760
+ request.security context ``sec_id``: ``timeframe.*`` reads that
1761
+ context's timeframe, as ``_build_security_timeframe_member`` lowers
1762
+ it for the rest of the payload."""
1763
+ if "script_tf_" not in cpp:
1764
+ return cpp
1765
+ tf = self._security_timeframe_expr(sec_id)
1766
+ if not self._TF_EXPR_ATOM.fullmatch(tf):
1767
+ tf = f"({tf})"
1768
+ # String literals are left alone.
1769
+ parts = re.split(r'("(?:\\.|[^"\\])*")', cpp)
1770
+ return "".join(
1771
+ part if index % 2 else re.sub(r"\bscript_tf_\b", lambda _m: tf, part)
1772
+ for index, part in enumerate(parts)
1773
+ )
1774
+
1775
+ def _ta_plan_cpp_type(self, site: "TACallSite", plan: dict | None) -> str:
1776
+ if plan is None:
1777
+ return site.class_name
1778
+ if plan["kind"] == "series_extreme":
1779
+ return self._TA_SERIES_EXTREME_CLASSES[plan["family"]]
1780
+ if plan["kind"] == "supertrend":
1781
+ return "pineforge::source::PineSupertrend"
1782
+ return f"pineforge::source::FirstCallBound<{site.class_name}>"
1783
+
1784
+ def _ta_member_cpp_type(self, site: "TACallSite") -> str:
1785
+ """The C++ type a site's member is declared with."""
1786
+ return self._ta_plan_cpp_type(site, self._ta_dynamic_plan(site))
1787
+
1788
+ def _ta_security_member_cpp_type(self, sec_id: int, site: "TACallSite",
1789
+ variant: dict) -> str:
1790
+ """The C++ type of one request.security copy of a site."""
1791
+ return self._ta_plan_cpp_type(
1792
+ site, self._ta_security_plan(sec_id, site, variant.get("binding_stack", ())))
1793
+
1794
+ def _ta_uses_dynamic_lengths(self) -> bool:
1795
+ """Whether any live site or request.security copy takes the lowering
1796
+ above (the TU then includes its engine header)."""
1797
+ dead = getattr(self, "_dead_ta_indices", set())
1798
+ if any(
1799
+ self._ta_dynamic_plan(site) is not None
1800
+ for index, site in enumerate(self.ctx.ta_call_sites)
1801
+ if index not in dead
1802
+ ):
1803
+ return True
1804
+ return any(
1805
+ self._ta_security_plan(
1806
+ info["sec_id"], self.ctx.ta_call_sites[idx],
1807
+ variant.get("binding_stack", ())) is not None
1808
+ for info in self._security_eval_info
1809
+ for idx, variants in (info.get("ta_variants") or {}).items()
1810
+ for variant in variants
1811
+ )
1812
+
1813
+ def _ta_ctor_param(self, site: "TACallSite", position: int):
1814
+ """The signature parameter a constructor argument binds (Param or None)."""
1815
+ from ..analyzer.tables import TA_MULTI_CTOR, TA_PERIOD_ARG
1816
+ from ..signatures import TA_FUNCTIONS
1817
+ family = self._ta_site_function(site)
1818
+ if family in TA_MULTI_CTOR:
1819
+ indices = list(TA_MULTI_CTOR[family])
1820
+ if family in ("pivothigh", "pivotlow") and len(site.ctor_args) == 2:
1821
+ indices = [1, 2]
1822
+ elif family in TA_PERIOD_ARG:
1823
+ indices = [TA_PERIOD_ARG[family]]
1824
+ else:
1825
+ return None
1826
+ func = TA_FUNCTIONS.get(family)
1827
+ if func is None or position >= len(indices):
1828
+ return None
1829
+ widest = max(func.signatures, key=lambda sig: len(sig.params))
1830
+ index = indices[position]
1831
+ if index >= len(widest.params):
1832
+ return None
1833
+ return widest.params[index]
1834
+
1835
+ def _ta_dynamic_ctor_arg(self, site: "TACallSite", plan: dict, position: int,
1836
+ render_node) -> str:
1837
+ """One constructor argument of a lowered site: a compile-time value, a
1838
+ runtime-reset expression, a context-free simple expression, or (a
1839
+ series argument) the call site's own rendering. A request.security
1840
+ copy reached through a helper call takes the argument that call
1841
+ binds (``ctor_args`` holds the first call site's); any request.security
1842
+ copy reads ``timeframe.*`` in its requested context."""
1843
+ if "bound_simple" in plan:
1844
+ value = plan["bound_simple"].get(position)
1845
+ elif position in plan.get("refused", ()):
1846
+ # A refused position takes its context-free spelling, else (a
1847
+ # series one) the call site's rendering: never the reset path's
1848
+ # reading, which one ``_ta_arg_takes_plan`` sends here would
1849
+ # have cast to an int length, on the chart's timeframe, at the
1850
+ # end of the bar.
1851
+ value = plan["simple"].get(position)
1852
+ else:
1853
+ arg = site.ctor_args[position]
1854
+ # The live input first: a compile-time resolution of an input-backed
1855
+ # argument is only its default (the constructor's placeholder).
1856
+ value = self._runtime_ctor_arg_for_reset(arg)
1857
+ if value is None:
1858
+ resolved = self._resolve_ta_ctor_arg(arg)
1859
+ if self._is_compile_time_value(resolved):
1860
+ value = resolved
1861
+ if value is None:
1862
+ value = plan["simple"].get(position)
1863
+ if value is None:
1864
+ value = render_node(site.ctor_nodes[position])
1865
+ elif plan.get("sec_id") is not None:
1866
+ value = self._ta_payload_cpp(value, plan["sec_id"])
1867
+ if self._ta_ctor_arg_is_bool(site, position):
1868
+ value = self._ta_ctor_bool_cpp(value)
1869
+ return value
1870
+
1871
+ def _ta_dynamic_compute_args(self, site: "TACallSite", plan: dict, base_args: str,
1872
+ render_node) -> str:
1873
+ """The ``compute()`` / ``recompute()`` arguments of a lowered site."""
1874
+ kind = plan["kind"]
1875
+ site = plan.get("site", site)
1876
+ if kind == "series_extreme":
1877
+ length = self._ta_dynamic_ctor_arg(site, plan, 0, render_node)
1878
+ return f"{base_args}, pineforge::source::ta_number({length})"
1879
+ if kind == "supertrend":
1880
+ factor = self._ta_dynamic_ctor_arg(site, plan, 0, render_node)
1881
+ period = self._ta_dynamic_ctor_arg(site, plan, 1, render_node)
1882
+ dynamic = (f"pineforge::source::ta_number({factor}), "
1883
+ f"pineforge::source::ta_number({period})")
1884
+ return f"{dynamic}, {base_args}" if base_args else dynamic
1885
+ family = plan["family"]
1886
+ ctor = []
1887
+ for position in range(len(site.ctor_args)):
1888
+ value = self._ta_dynamic_ctor_arg(site, plan, position, render_node)
1889
+ param = self._ta_ctor_param(site, position)
1890
+ if (position in plan["refused"] and param is not None
1891
+ and param.pine_type == PineType.INT
1892
+ and not self._ta_ctor_arg_is_bool(site, position)):
1893
+ value = (f'pineforge::source::simple_ta_length({value}, '
1894
+ f'"{family}", "{param.name}")')
1895
+ ctor.append(value)
1896
+ make = f"[&]() {{ return {site.class_name}({', '.join(ctor)}); }}"
1897
+ return f"{make}, {base_args}" if base_args else make
1898
+
1899
+ def _simple_ta_top_level_decls(self) -> dict:
1900
+ """Top-level plain declarations read as simple values: declared once,
1901
+ never reassigned, not ``var``/``varip``, not declared ``series``."""
1902
+ cached = self.__dict__.get("_simple_ta_decls_cache")
1903
+ if cached is not None:
1904
+ return cached
1905
+ reassigned = self._find_reassigned_vars()
1906
+ counts: dict[str, int] = {}
1907
+ decls: dict[str, VarDecl] = {}
1908
+ for stmt in self.ctx.ast.body or []:
1909
+ if isinstance(stmt, VarDecl):
1910
+ counts[stmt.name] = counts.get(stmt.name, 0) + 1
1911
+ decls[stmt.name] = stmt
1912
+ elif isinstance(stmt, TupleAssign):
1913
+ for name in getattr(stmt, "names", []) or []:
1914
+ counts[name] = counts.get(name, 0) + 2
1915
+ result = {
1916
+ name: decl for name, decl in decls.items()
1917
+ if counts.get(name) == 1 and name not in reassigned
1918
+ and not decl.is_var and not decl.is_varip and decl.value is not None
1919
+ and (decl.annotations or {}).get("qualifier") != "series"
1920
+ }
1921
+ self.__dict__["_simple_ta_decls_cache"] = result
1922
+ return result
1923
+
1924
+ def _ta_locally_declared_names(self) -> frozenset:
1925
+ """Every name a block, loop or callable declares: a parameter, a loop
1926
+ variable, a local."""
1927
+ cached = self.__dict__.get("_ta_local_names_cache")
1928
+ if cached is not None:
1929
+ return cached
1930
+ top_level = {id(stmt) for stmt in self.ctx.ast.body or []}
1931
+ names: set[str] = set()
1932
+ for node in self._walk_ast(self.ctx.ast):
1933
+ if isinstance(node, (FuncDef, MethodDef)):
1934
+ names.update(p for p in node.params if isinstance(p, str))
1935
+ elif isinstance(node, VarDecl) and id(node) not in top_level:
1936
+ names.add(node.name)
1937
+ elif isinstance(node, TupleAssign) and id(node) not in top_level:
1938
+ names.update(name for name in node.names if name)
1939
+ elif isinstance(node, ForStmt) and node.var:
1940
+ names.add(node.var)
1941
+ elif isinstance(node, ForInStmt):
1942
+ if node.var:
1943
+ names.add(node.var)
1944
+ names.update(name for name in node.vars or [] if name)
1945
+ result = frozenset(names)
1946
+ self.__dict__["_ta_local_names_cache"] = result
1947
+ return result
1948
+
1949
+ def _ta_identifier_reads_top_level(self, node: Identifier, scoped: bool) -> bool:
1950
+ """Whether ``node`` reads the top-level binding of its name. A node of
1951
+ the analysed program (``scoped``) answers from the scope the analyzer
1952
+ resolved it in; a spelling re-parsed from ``ctor_args`` has none, so
1953
+ its name must be one no block, loop or callable declares."""
1954
+ if scoped:
1955
+ scopes = getattr(self.ctx, "identifier_binding_scopes", {}) or {}
1956
+ return scopes.get(id(node)) == "global"
1957
+ return node.name not in self._ta_locally_declared_names()
1958
+
1959
+ def _simple_ta_arg_ast(self, node, seen: frozenset = frozenset(), depth: int = 0,
1960
+ scoped: bool = False):
1961
+ """A context-free copy of ``node`` whose leaves are literals, inputs,
1962
+ ``syminfo.*`` / ``timeframe.*`` metadata and ``math`` constants
1963
+ combined by operators, ternaries, casts and pure ``math.*`` /
1964
+ ``str.*`` calls, top-level simple declarations expanded in place --
1965
+ a Pine simple value. None when any part may vary by bar, or when an
1966
+ identifier may be bound by a block, loop or callable rather than the
1967
+ top-level declaration (``_ta_identifier_reads_top_level``)."""
1968
+ if node is None or depth > 64:
1969
+ return None
1970
+ if isinstance(node, (NumberLiteral, StringLiteral, BoolLiteral, NaLiteral)):
1971
+ return copy.deepcopy(node)
1972
+ if isinstance(node, Identifier):
1973
+ name = node.name
1974
+ if self._known_var_is_lexically_shadowed(name):
1975
+ return None
1976
+ if not self._ta_identifier_reads_top_level(node, scoped):
1977
+ return None
1978
+ # A derived name tracked only since string literals are spelled
1979
+ # (``_ta_length_literal_facts``) is expanded from its declaration,
1980
+ # as before, not read from the chart's member.
1981
+ literal_derived = (name in self._derived_input_expr
1982
+ and self._ta_length_literal_facts(name, inline=False)[0])
1983
+ if not literal_derived and (name in self._input_backed_vars
1984
+ or name in self._known_vars):
1985
+ return copy.deepcopy(node)
1986
+ decl = self._simple_ta_top_level_decls().get(name)
1987
+ if decl is None or name in seen:
1988
+ return None
1989
+ # The declaration's own nodes carry the analyzer's scope.
1990
+ return self._simple_ta_arg_ast(decl.value, seen | {name}, depth + 1, True)
1991
+ if isinstance(node, MemberAccess):
1992
+ if not isinstance(node.object, Identifier):
1993
+ return None
1994
+ namespace = node.object.name
1995
+ if (namespace == "syminfo"
1996
+ or (namespace == "timeframe" and node.member in self._TF_STABLE_MEMBERS)
1997
+ or (namespace == "math" and node.member in self._MATH_STABLE_MEMBERS)):
1998
+ return copy.deepcopy(node)
1999
+ return None
2000
+ if isinstance(node, BinOp):
2001
+ left = self._simple_ta_arg_ast(node.left, seen, depth + 1, scoped)
2002
+ right = self._simple_ta_arg_ast(node.right, seen, depth + 1, scoped)
2003
+ if left is None or right is None:
2004
+ return None
2005
+ out = copy.copy(node)
2006
+ out.left, out.right = left, right
2007
+ return out
2008
+ if isinstance(node, UnaryOp):
2009
+ operand = self._simple_ta_arg_ast(node.operand, seen, depth + 1, scoped)
2010
+ if operand is None:
2011
+ return None
2012
+ out = copy.copy(node)
2013
+ out.operand = operand
2014
+ return out
2015
+ if isinstance(node, Ternary):
2016
+ parts = [self._simple_ta_arg_ast(child, seen, depth + 1, scoped)
2017
+ for child in (node.condition, node.true_val, node.false_val)]
2018
+ if any(part is None for part in parts):
2019
+ return None
2020
+ out = copy.copy(node)
2021
+ out.condition, out.true_val, out.false_val = parts
2022
+ return out
2023
+ if isinstance(node, FuncCall):
2024
+ func_name, namespace = self._resolve_callee(node.callee)
2025
+ if namespace == "input" or (namespace is None and func_name == "input"):
2026
+ return copy.deepcopy(node) if self._is_stable_inline_input(node) else None
2027
+ pure = (
2028
+ (namespace == "str" and func_name in self._SIMPLE_STR_FUNCS)
2029
+ or (namespace == "math" and func_name in self._MATH_STABLE_MEMBERS)
2030
+ or (namespace == "timeframe" and func_name in self._TF_STABLE_MEMBERS)
2031
+ or (namespace is None and func_name in ("int", "float", "bool", "string"))
2032
+ )
2033
+ if not pure:
2034
+ return None
2035
+ args = [self._simple_ta_arg_ast(arg, seen, depth + 1, scoped) for arg in node.args]
2036
+ kwargs = {key: self._simple_ta_arg_ast(value, seen, depth + 1, scoped)
2037
+ for key, value in (node.kwargs or {}).items()}
2038
+ if any(arg is None for arg in args) or any(v is None for v in kwargs.values()):
2039
+ return None
2040
+ out = copy.copy(node)
2041
+ out.callee = copy.deepcopy(node.callee)
2042
+ out.args = args
2043
+ out.kwargs = kwargs
2044
+ return out
2045
+ return None
2046
+
2047
+ def _simple_ta_arg_cpp_node(self, node, scoped: bool = False) -> str | None:
2048
+ """``_simple_ta_arg_cpp`` of an AST node (not cached); ``scoped`` when
2049
+ the node belongs to the analysed program
2050
+ (``_ta_identifier_reads_top_level``)."""
2051
+ expanded = self._simple_ta_arg_ast(node, scoped=scoped) if node is not None else None
2052
+ if expanded is None:
2053
+ return None
2054
+ previous = self._reset_input_getter_mode
2055
+ self._reset_input_getter_mode = True
2056
+ try:
2057
+ rendered = self._visit_expr(expanded)
2058
+ except Exception:
2059
+ rendered = None
2060
+ finally:
2061
+ self._reset_input_getter_mode = previous
2062
+ if rendered and "/* " not in rendered:
2063
+ return f"({rendered})"
2064
+ return None
2065
+
2066
+ def _simple_ta_arg_cpp(self, arg_str: str) -> str | None:
2067
+ """The context-free C++ of a simple constructor argument spelled
2068
+ ``arg_str`` (inputs read through their override-aware getters, so it
2069
+ evaluates the same in ``on_bar`` and in ``evaluate_security`` before
2070
+ either initialised anything), or None when the argument is series.
2071
+ The spelling carries no scope: the answer depends on the text alone."""
2072
+ cache = self.__dict__.setdefault("_simple_ta_arg_cache", {})
2073
+ if arg_str in cache:
2074
+ return cache[arg_str]
2075
+ try:
2076
+ from ..lexer import Lexer
2077
+ from ..parser import Parser
2078
+ node = Parser(Lexer(arg_str).tokenize(), source=arg_str)._parse_expression()
2079
+ except Exception:
2080
+ node = None
2081
+ result = self._simple_ta_arg_cpp_node(node)
2082
+ cache[arg_str] = result
2083
+ return result
2084
+
997
2085
  # ------------------------------------------------------------------
998
2086
  # Compile-time-value predicate (paired with the runtime-reset chain
999
2087
  # that still lives on CodeGen because it uses Python's expression