@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.
- package/README.md +16 -16
- package/glue.py +24 -16
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +125 -34
- package/pineforge_codegen/analyzer/__init__.py +2 -0
- package/pineforge_codegen/analyzer/base.py +767 -77
- package/pineforge_codegen/analyzer/call_handlers.py +268 -42
- package/pineforge_codegen/analyzer/contracts.py +37 -0
- package/pineforge_codegen/analyzer/diagnostics.py +30 -4
- package/pineforge_codegen/analyzer/tables.py +49 -8
- package/pineforge_codegen/analyzer/types.py +33 -1
- package/pineforge_codegen/ast_nodes.py +32 -1
- package/pineforge_codegen/block_locals.py +185 -0
- package/pineforge_codegen/builtin_keywords.py +42 -0
- package/pineforge_codegen/codegen/base.py +907 -159
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +961 -213
- package/pineforge_codegen/codegen/helpers.py +435 -14
- package/pineforge_codegen/codegen/host_members.py +162 -0
- package/pineforge_codegen/codegen/input.py +252 -85
- package/pineforge_codegen/codegen/security.py +4372 -377
- package/pineforge_codegen/codegen/session_market.py +71 -0
- package/pineforge_codegen/codegen/ta.py +1198 -101
- package/pineforge_codegen/codegen/tables.py +205 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +2055 -62
- package/pineforge_codegen/codegen/visit_call.py +929 -131
- package/pineforge_codegen/codegen/visit_expr.py +765 -63
- package/pineforge_codegen/codegen/visit_stmt.py +610 -52
- package/pineforge_codegen/external_requests.py +877 -0
- package/pineforge_codegen/lexer.py +104 -22
- package/pineforge_codegen/library_inline.py +1304 -0
- package/pineforge_codegen/library_modules.py +126 -0
- package/pineforge_codegen/library_v5.py +683 -0
- package/pineforge_codegen/limits.py +138 -0
- package/pineforge_codegen/method_binding.py +33 -0
- package/pineforge_codegen/parser.py +384 -68
- package/pineforge_codegen/pine_libraries.py +266 -0
- package/pineforge_codegen/pine_spelling.py +216 -0
- package/pineforge_codegen/pragmas.py +64 -10
- package/pineforge_codegen/security_contexts.py +1585 -0
- package/pineforge_codegen/session_reads.py +84 -0
- package/pineforge_codegen/signatures.py +48 -23
- package/pineforge_codegen/support_checker.py +1106 -85
- package/pineforge_codegen/symbols.py +4 -2
- package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +23 -21
- package/transpile.worker.mjs +24 -16
- package/pineforge_codegen-0.10.3.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,16 @@ from ..ast_nodes import (
|
|
|
31
33
|
Ternary, TupleAssign, TupleLiteral, TypeDecl, TypeField, UnaryOp, VarDecl,
|
|
32
34
|
WhileStmt,
|
|
33
35
|
)
|
|
34
|
-
from
|
|
36
|
+
from ..pine_spelling import blank_string_literals
|
|
37
|
+
from ..symbols import PineType
|
|
38
|
+
from .helpers import pine_index_int_cast
|
|
39
|
+
from .tables import (
|
|
40
|
+
BAR_SERIES_PUSH,
|
|
41
|
+
TA_CHART_PREV_CLOSE,
|
|
42
|
+
TA_CHART_PREV_CLOSE_ARG,
|
|
43
|
+
TA_IMPLICIT_APPEND,
|
|
44
|
+
TA_IMPLICIT_COMPUTE_FULL,
|
|
45
|
+
)
|
|
35
46
|
|
|
36
47
|
if TYPE_CHECKING:
|
|
37
48
|
from ..analyzer import TACallSite
|
|
@@ -95,7 +106,52 @@ class TaSiteHelper:
|
|
|
95
106
|
# .compute() arg-string construction
|
|
96
107
|
# ------------------------------------------------------------------
|
|
97
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
|
+
|
|
98
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:
|
|
99
155
|
"""Build the C++ argument string for ``<member>.compute(...)`` of a TA site.
|
|
100
156
|
|
|
101
157
|
Three layered cases:
|
|
@@ -111,17 +167,55 @@ class TaSiteHelper:
|
|
|
111
167
|
when applicable (e.g. ``vwma()`` -> ``volume`` only)."""
|
|
112
168
|
ta_name = self._ta_name_from_site(site)
|
|
113
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
|
+
|
|
114
198
|
if ta_name in TA_IMPLICIT_COMPUTE_FULL:
|
|
115
199
|
implicit = TA_IMPLICIT_COMPUTE_FULL[ta_name]
|
|
200
|
+
# issue #178: chart-context atr / tr take the previous CHART
|
|
201
|
+
# bar's close as a 4th argument (see TA_CHART_PREV_CLOSE).
|
|
202
|
+
if ta_name in TA_CHART_PREV_CLOSE:
|
|
203
|
+
implicit = f"{implicit}, {TA_CHART_PREV_CLOSE_ARG}"
|
|
116
204
|
if site.compute_args:
|
|
117
|
-
explicit = ", ".join(
|
|
205
|
+
explicit = ", ".join(
|
|
206
|
+
self._ta_compute_arg_cpp(ta_name, i, a)
|
|
207
|
+
for i, a in enumerate(site.compute_args)
|
|
208
|
+
)
|
|
118
209
|
if ta_name in self._TA_IMPLICIT_REPLACE:
|
|
119
210
|
return explicit
|
|
120
211
|
return f"{explicit}, {implicit}" if explicit else implicit
|
|
121
212
|
return implicit
|
|
122
213
|
|
|
123
214
|
if site.compute_args:
|
|
124
|
-
explicit = ", ".join(
|
|
215
|
+
explicit = ", ".join(
|
|
216
|
+
self._ta_compute_arg_cpp(ta_name, i, a)
|
|
217
|
+
for i, a in enumerate(site.compute_args)
|
|
218
|
+
)
|
|
125
219
|
if ta_name in TA_IMPLICIT_APPEND:
|
|
126
220
|
return f"{explicit}, {TA_IMPLICIT_APPEND[ta_name]}"
|
|
127
221
|
return explicit
|
|
@@ -383,48 +477,6 @@ class TaSiteHelper:
|
|
|
383
477
|
"ycelestine77/quantbyboji exact on main, 2026-09-04)"
|
|
384
478
|
)
|
|
385
479
|
|
|
386
|
-
_LAZY_SOURCE_CLOCK_BAR_SOURCES = frozenset({
|
|
387
|
-
"open", "high", "low", "close", "volume", "hl2", "hlc3", "ohlc4", "hlcc4",
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
def _script_has_user_binding(self, name: str) -> bool:
|
|
391
|
-
cache = getattr(self, "_user_binding_names", None)
|
|
392
|
-
if cache is None:
|
|
393
|
-
cache = set()
|
|
394
|
-
ast = getattr(self.ctx, "ast", None)
|
|
395
|
-
for node in self._walk_ast(ast):
|
|
396
|
-
if isinstance(node, VarDecl):
|
|
397
|
-
cache.add(node.name)
|
|
398
|
-
elif isinstance(node, TupleAssign):
|
|
399
|
-
cache.update(node.names)
|
|
400
|
-
elif isinstance(node, ForStmt):
|
|
401
|
-
cache.add(node.var)
|
|
402
|
-
elif isinstance(node, ForInStmt):
|
|
403
|
-
if node.var:
|
|
404
|
-
cache.add(node.var)
|
|
405
|
-
cache.update(node.vars or ())
|
|
406
|
-
elif isinstance(node, FuncDef):
|
|
407
|
-
cache.add(node.name)
|
|
408
|
-
cache.update(node.params)
|
|
409
|
-
self._user_binding_names = cache
|
|
410
|
-
return name in cache
|
|
411
|
-
|
|
412
|
-
def _lazy_source_clock_chart_source(self, site: "TACallSite"):
|
|
413
|
-
"""The unshadowed bar-builtin source node, or None.
|
|
414
|
-
|
|
415
|
-
Between two executions closer than ``length`` bars the pin does not
|
|
416
|
-
distinguish the held read from #64's eager chart ``source[length]``;
|
|
417
|
-
the eager read is kept where the source is a plain chart builtin.
|
|
418
|
-
"""
|
|
419
|
-
source = site.compute_args[0] if site.compute_args else None
|
|
420
|
-
if (
|
|
421
|
-
isinstance(source, Identifier)
|
|
422
|
-
and source.name in self._LAZY_SOURCE_CLOCK_BAR_SOURCES
|
|
423
|
-
and not self._script_has_user_binding(source.name)
|
|
424
|
-
):
|
|
425
|
-
return source
|
|
426
|
-
return None
|
|
427
|
-
|
|
428
480
|
@staticmethod
|
|
429
481
|
def _lazy_source_clock_length_node(node):
|
|
430
482
|
kwargs = getattr(node, "kwargs", None) or {}
|
|
@@ -494,16 +546,9 @@ class TaSiteHelper:
|
|
|
494
546
|
clocks: dict[int, dict] = {}
|
|
495
547
|
routed = self._lazy_edge_ta_hoist_plan()["source_clock"]
|
|
496
548
|
for index, (node_id, info) in enumerate(routed.items(), start=1):
|
|
497
|
-
chart_source = self._lazy_source_clock_chart_source(info["site"])
|
|
498
549
|
clocks[node_id] = {
|
|
499
550
|
"clock": allocate(f"_pf_lazy_src_clock_{index}", reserved_members),
|
|
500
551
|
"hist": allocate(f"_pf_lazy_src_hist_{index}", reserved_members),
|
|
501
|
-
"chart": (
|
|
502
|
-
allocate(f"_pf_lazy_src_chart_{index}", reserved_members)
|
|
503
|
-
if chart_source is not None
|
|
504
|
-
else None
|
|
505
|
-
),
|
|
506
|
-
"chart_source": chart_source,
|
|
507
552
|
"site": info["site"],
|
|
508
553
|
"node": info["node"],
|
|
509
554
|
"length_literal": info["length_literal"],
|
|
@@ -516,22 +561,22 @@ class TaSiteHelper:
|
|
|
516
561
|
source = self._visit_expr(site.compute_args[0])
|
|
517
562
|
length_node = self._lazy_source_clock_length_node(node)
|
|
518
563
|
literal = self._lazy_source_clock_length_literal(length_node)
|
|
519
|
-
chart = info["chart"]
|
|
520
564
|
if literal is not None:
|
|
521
565
|
length_expr = str(literal)
|
|
522
566
|
held = f"{info['hist']}[{literal - 1}]" if literal >= 1 else "na<double>()"
|
|
523
|
-
eager = f"{chart}[{literal}]" if chart is not None else held
|
|
524
567
|
else:
|
|
525
|
-
|
|
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)
|
|
526
575
|
held = (
|
|
527
576
|
f"(({length_expr}) >= 1 ? {info['hist']}[({length_expr}) - 1] "
|
|
528
577
|
f": na<double>())"
|
|
529
578
|
)
|
|
530
|
-
|
|
531
|
-
previous = (
|
|
532
|
-
f"{info['clock']}.previous_source({held}, {eager}, {length_expr}, "
|
|
533
|
-
f"bar_index_)"
|
|
534
|
-
)
|
|
579
|
+
previous = f"{info['clock']}.previous_source({held}, {length_expr})"
|
|
535
580
|
method = "roc" if self._ta_name_from_site(site) == "roc" else "change"
|
|
536
581
|
return f"{info['clock']}.{method}({source}, {previous})"
|
|
537
582
|
|
|
@@ -549,10 +594,8 @@ class TaSiteHelper:
|
|
|
549
594
|
"// history is na before the first execution. The paired hist Series",
|
|
550
595
|
"// holds `bar_base_source` (the value committed by earlier bars) once",
|
|
551
596
|
"// per chart bar, so `hist[length - 1]` is the source at the most",
|
|
552
|
-
"// recent execution at or before bar-length
|
|
553
|
-
"// closer than `length` bars
|
|
554
|
-
"// from the eager chart `source[length]`, so the #64 eager read is",
|
|
555
|
-
"// 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.",
|
|
556
599
|
f"struct {type_name} {{",
|
|
557
600
|
" double committed_source = na<double>();",
|
|
558
601
|
" int committed_bar = -1;",
|
|
@@ -578,12 +621,11 @@ class TaSiteHelper:
|
|
|
578
621
|
" }",
|
|
579
622
|
" }",
|
|
580
623
|
"",
|
|
581
|
-
" double previous_source(double held,
|
|
582
|
-
" int bar) const {",
|
|
624
|
+
" double previous_source(double held, int length) const {",
|
|
583
625
|
" if (bar_base_bar < 0 || length < 1) {",
|
|
584
626
|
" return na<double>();",
|
|
585
627
|
" }",
|
|
586
|
-
" return
|
|
628
|
+
" return held;",
|
|
587
629
|
" }",
|
|
588
630
|
"",
|
|
589
631
|
" double change(double source, double previous) {",
|
|
@@ -642,6 +684,10 @@ class TaSiteHelper:
|
|
|
642
684
|
and request.security sites retain their existing behavior."""
|
|
643
685
|
if not getattr(site, "is_static", False):
|
|
644
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
|
|
645
691
|
node = getattr(site, "node", None)
|
|
646
692
|
plan = self._lazy_edge_ta_hoist_plan()
|
|
647
693
|
if node is not None and (
|
|
@@ -858,29 +904,78 @@ class TaSiteHelper:
|
|
|
858
904
|
return
|
|
859
905
|
# Literals, identifiers and anything else are leaves.
|
|
860
906
|
|
|
861
|
-
def roots(stmt) -> list:
|
|
862
|
-
if isinstance(stmt, VarDecl):
|
|
863
|
-
if stmt.is_var or stmt.is_varip:
|
|
864
|
-
return []
|
|
865
|
-
return [stmt.value]
|
|
866
|
-
if isinstance(stmt, Assignment):
|
|
867
|
-
return [stmt.target, stmt.value]
|
|
868
|
-
if isinstance(stmt, TupleAssign):
|
|
869
|
-
return [stmt.value]
|
|
870
|
-
if isinstance(stmt, ExprStmt):
|
|
871
|
-
return [stmt.expr]
|
|
872
|
-
if isinstance(stmt, IfStmt):
|
|
873
|
-
return [stmt.condition]
|
|
874
|
-
return []
|
|
875
|
-
|
|
876
907
|
ast = getattr(self.ctx, "ast", None)
|
|
877
908
|
for stmt in getattr(ast, "body", ()) or ():
|
|
878
909
|
units: list[dict] = []
|
|
879
|
-
for root in
|
|
910
|
+
for root in self._lazy_edge_statement_roots(stmt):
|
|
880
911
|
scan(root, False, units)
|
|
881
912
|
if units:
|
|
882
913
|
by_stmt[id(stmt)] = units
|
|
883
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
|
+
|
|
884
979
|
plan = {
|
|
885
980
|
"by_stmt": by_stmt,
|
|
886
981
|
"call_nodes": call_nodes,
|
|
@@ -925,6 +1020,214 @@ class TaSiteHelper:
|
|
|
925
1020
|
self._hoisted_ta_values.clear()
|
|
926
1021
|
self._hoisted_hist_reads.clear()
|
|
927
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
|
+
|
|
928
1231
|
def _security_ta_compute_args_for_site(
|
|
929
1232
|
self,
|
|
930
1233
|
sec_id: int,
|
|
@@ -933,6 +1236,38 @@ class TaSiteHelper:
|
|
|
933
1236
|
security_mutable_names: set[str] | None = None,
|
|
934
1237
|
helper_binding_stack: tuple[dict, ...] | None = None,
|
|
935
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,
|
|
936
1271
|
) -> str:
|
|
937
1272
|
"""Same as ``_ta_compute_args_for_site`` but inside an ``evaluate_security`` body.
|
|
938
1273
|
|
|
@@ -942,20 +1277,87 @@ class TaSiteHelper:
|
|
|
942
1277
|
mutable globals get rebound to the security-context shadows."""
|
|
943
1278
|
ta_name = self._ta_name_from_site(site)
|
|
944
1279
|
|
|
945
|
-
if
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
explicit = ", ".join(
|
|
1280
|
+
if site.class_name in ("_PFAnchoredVWAP", "_PFAnchoredVWAPBands"):
|
|
1281
|
+
explicit = [
|
|
1282
|
+
self._coerce_bool_expr(
|
|
949
1283
|
self._build_security_expr(
|
|
950
|
-
sec_id,
|
|
951
|
-
a,
|
|
952
|
-
None,
|
|
953
|
-
ta_results,
|
|
1284
|
+
sec_id, a, None, ta_results,
|
|
954
1285
|
security_mutable_names=security_mutable_names,
|
|
955
1286
|
helper_binding_stack=helper_binding_stack,
|
|
956
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,
|
|
1311
|
+
security_mutable_names=security_mutable_names,
|
|
1312
|
+
helper_binding_stack=helper_binding_stack,
|
|
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
|
+
)
|
|
957
1359
|
)
|
|
958
|
-
for a in site.compute_args
|
|
1360
|
+
for i, a in enumerate(site.compute_args)
|
|
959
1361
|
)
|
|
960
1362
|
if ta_name in self._TA_IMPLICIT_REPLACE:
|
|
961
1363
|
return explicit
|
|
@@ -964,16 +1366,31 @@ class TaSiteHelper:
|
|
|
964
1366
|
|
|
965
1367
|
if site.compute_args:
|
|
966
1368
|
explicit = ", ".join(
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
+
)
|
|
975
1392
|
)
|
|
976
|
-
for a in site.compute_args
|
|
1393
|
+
for i, a in enumerate(site.compute_args)
|
|
977
1394
|
)
|
|
978
1395
|
if ta_name in TA_IMPLICIT_APPEND:
|
|
979
1396
|
implicit = TA_IMPLICIT_APPEND[ta_name].replace("current_bar_.", "bar.")
|
|
@@ -985,6 +1402,686 @@ class TaSiteHelper:
|
|
|
985
1402
|
|
|
986
1403
|
return ""
|
|
987
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
|
+
|
|
988
2085
|
# ------------------------------------------------------------------
|
|
989
2086
|
# Compile-time-value predicate (paired with the runtime-reset chain
|
|
990
2087
|
# that still lives on CodeGen because it uses Python's expression
|