@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.
- 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 +754 -76
- package/pineforge_codegen/analyzer/call_handlers.py +260 -40
- 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 +896 -156
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +946 -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 +1188 -100
- package/pineforge_codegen/codegen/tables.py +193 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +1882 -78
- package/pineforge_codegen/codegen/visit_call.py +920 -131
- package/pineforge_codegen/codegen/visit_expr.py +738 -57
- package/pineforge_codegen/codegen/visit_stmt.py +595 -49
- 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-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.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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
562
|
-
"// closer than `length` bars
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
955
|
-
|
|
956
|
-
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|