@pineforge/codegen-pyodide 0.10.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.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
|
@@ -47,7 +47,7 @@ TA_CLASS_MAP = {
|
|
|
47
47
|
"dmi": "ta::DMI",
|
|
48
48
|
"sar": "ta::SAR",
|
|
49
49
|
"bb": "ta::BB",
|
|
50
|
-
"kc": "
|
|
50
|
+
"kc": "_PFKC",
|
|
51
51
|
"wma": "ta::WMA",
|
|
52
52
|
"hma": "ta::HMA",
|
|
53
53
|
"stdev": "ta::StdDev",
|
|
@@ -72,7 +72,7 @@ TA_CLASS_MAP = {
|
|
|
72
72
|
"highestbars": "ta::HighestBars",
|
|
73
73
|
"lowestbars": "ta::LowestBars",
|
|
74
74
|
# Batch 4 — remaining TA functions
|
|
75
|
-
"alma": "
|
|
75
|
+
"alma": "_PFALMA",
|
|
76
76
|
"swma": "ta::SWMA",
|
|
77
77
|
"mfi": "ta::MFI",
|
|
78
78
|
"cmo": "ta::CMO",
|
|
@@ -80,7 +80,7 @@ TA_CLASS_MAP = {
|
|
|
80
80
|
"wpr": "ta::WPR",
|
|
81
81
|
"cog": "ta::COG",
|
|
82
82
|
"bbw": "ta::BBW",
|
|
83
|
-
"kcw": "
|
|
83
|
+
"kcw": "_PFKCW",
|
|
84
84
|
"barssince": "ta::BarsSince",
|
|
85
85
|
"valuewhen": "ta::ValueWhen",
|
|
86
86
|
"correlation": "ta::Correlation",
|
|
@@ -90,6 +90,8 @@ TA_CLASS_MAP = {
|
|
|
90
90
|
"vwap": "ta::VWAP",
|
|
91
91
|
# 3-arg form: ta.vwap(source, anchor, stdev_mult) → tuple [vwap, upper, lower]
|
|
92
92
|
"vwap_bands": "ta::VWAPBands",
|
|
93
|
+
"vwap_anchored": "_PFAnchoredVWAP",
|
|
94
|
+
"vwap_anchored_bands": "_PFAnchoredVWAPBands",
|
|
93
95
|
"obv": "ta::OBV",
|
|
94
96
|
"accdist": "ta::AccDist",
|
|
95
97
|
"nvi": "ta::NVI",
|
|
@@ -126,10 +128,14 @@ TA_PERIOD_ARG = {
|
|
|
126
128
|
# Task 5
|
|
127
129
|
"mode": 1, "range": 1, "dev": 1,
|
|
128
130
|
"rci": 1,
|
|
131
|
+
# ta::ValueWhen keeps max(1, max_occurrence + 1) values, its constructor's
|
|
132
|
+
# bound; compute() reads values_[occurrence]. See TA_COMPUTE_ARGS.
|
|
133
|
+
"valuewhen": 2,
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
# Functions that return tuples
|
|
132
|
-
TA_TUPLE_RETURNS = {"macd", "supertrend", "dmi", "bb", "kc", "vwap_bands"
|
|
137
|
+
TA_TUPLE_RETURNS = {"macd", "supertrend", "dmi", "bb", "kc", "vwap_bands",
|
|
138
|
+
"vwap_anchored_bands"}
|
|
133
139
|
TA_TUPLE_ELEMENT_COUNTS = {
|
|
134
140
|
"macd": 3,
|
|
135
141
|
"supertrend": 2,
|
|
@@ -137,6 +143,7 @@ TA_TUPLE_ELEMENT_COUNTS = {
|
|
|
137
143
|
"bb": 3,
|
|
138
144
|
"kc": 3,
|
|
139
145
|
"vwap_bands": 3,
|
|
146
|
+
"vwap_anchored_bands": 3,
|
|
140
147
|
}
|
|
141
148
|
|
|
142
149
|
# Functions with multiple constructor args
|
|
@@ -146,12 +153,12 @@ TA_MULTI_CTOR = {
|
|
|
146
153
|
"supertrend": [0, 1], # factor, atr_period
|
|
147
154
|
"dmi": [0, 1], # di_length, adx_smoothing
|
|
148
155
|
"bb": [1, 2], # length, mult
|
|
149
|
-
"kc": [1, 2],
|
|
156
|
+
"kc": [1, 2, 3], # length, mult, useTrueRange
|
|
150
157
|
"sar": [0, 1, 2], # start, increment, maximum
|
|
151
158
|
"pivothigh": [0, 1], # left_bars, right_bars
|
|
152
159
|
"pivotlow": [0, 1], # left_bars, right_bars
|
|
153
160
|
# Batch 4
|
|
154
|
-
"alma": [1, 2, 3],
|
|
161
|
+
"alma": [1, 2, 3, 4], # length, offset, sigma, floor
|
|
155
162
|
"mfi": [1], # length (src is compute arg, vol implicit)
|
|
156
163
|
# Pine signature: ta.tsi(source, short_length, long_length) — positions
|
|
157
164
|
# 1 and 2 carry the lengths that initialize the four nested EMAs;
|
|
@@ -162,7 +169,8 @@ TA_MULTI_CTOR = {
|
|
|
162
169
|
"tsi": [1, 2], # short_length, long_length
|
|
163
170
|
"wpr": [0], # length
|
|
164
171
|
"bbw": [1, 2], # length, mult
|
|
165
|
-
"kcw": [1, 2],
|
|
172
|
+
"kcw": [1, 2, 3], # length, mult, useTrueRange
|
|
173
|
+
"vwap_anchored_bands": [2], # source, anchor, stdev_mult
|
|
166
174
|
"tr": [0], # handle_na (compile-time bool)
|
|
167
175
|
"stdev": [1, 2], # length, biased
|
|
168
176
|
"variance": [1, 2], # length, biased
|
|
@@ -174,10 +182,43 @@ TA_MULTI_CTOR = {
|
|
|
174
182
|
TA_COMPUTE_ARGS = {
|
|
175
183
|
"stdev": [0],
|
|
176
184
|
"variance": [0],
|
|
185
|
+
# ta::Change's constructor only bounds the kept history; compute() reads
|
|
186
|
+
# the lookback from its own ``length`` argument (default 1), so the length
|
|
187
|
+
# goes to both. Without it ``ta.change(src, 14)`` was a one-bar change.
|
|
188
|
+
"change": [0, 1],
|
|
189
|
+
# The mirror image: compute(condition, source, occurrence) always had the
|
|
190
|
+
# occurrence, but the history bound is the constructor's max_occurrence
|
|
191
|
+
# (default 1, two values kept), so every occurrence >= 2 read na.
|
|
192
|
+
"valuewhen": [0, 1, 2],
|
|
193
|
+
# ``ta.vwap(source, anchor)`` is split into a dedicated anchored site in
|
|
194
|
+
# call_handlers; this entry remains for the omitted-anchor scalar form.
|
|
195
|
+
"vwap": [0],
|
|
196
|
+
# ``ta.alma(series, length, offset, sigma, floor)``: ta::ALMA takes no
|
|
197
|
+
# floor (m = offset * (length - 1), unfloored), the value the support
|
|
198
|
+
# checker admits (false); ``floor`` used to reach ALMA::compute(src).
|
|
199
|
+
"alma": [0],
|
|
200
|
+
# ``ta.kc`` / ``ta.kcw(series, length, mult, useTrueRange)``: ta::KC
|
|
201
|
+
# always averages the true range, the value the support checker admits
|
|
202
|
+
# (true); ``useTrueRange`` used to reach a compute() overload that does
|
|
203
|
+
# not exist. high / low / close are appended implicitly.
|
|
204
|
+
"kc": [0],
|
|
205
|
+
"kcw": [0],
|
|
206
|
+
"vwap_anchored": [0, 1],
|
|
207
|
+
"vwap_anchored_bands": [0, 1],
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# The one-argument forms ``ta.highest(length)`` / ``ta.lowest(length)`` /
|
|
211
|
+
# ``ta.highestbars(length)`` / ``ta.lowestbars(length)`` (positional or
|
|
212
|
+
# ``length=``) read this bar field as their source (TradingView: "One arg
|
|
213
|
+
# version: length is the number of bars back. Algorithm uses high as a source
|
|
214
|
+
# series.").
|
|
215
|
+
TA_LENGTH_ONLY_DEFAULT_SOURCE = {
|
|
216
|
+
"highest": "high", "lowest": "low",
|
|
217
|
+
"highestbars": "high", "lowestbars": "low",
|
|
177
218
|
}
|
|
178
219
|
|
|
179
220
|
# No-state functions (no constructor args, stateless or self-contained)
|
|
180
|
-
TA_NO_CTOR = {"crossover", "crossunder", "cross", "cum", "swma", "barssince",
|
|
221
|
+
TA_NO_CTOR = {"crossover", "crossunder", "cross", "cum", "swma", "barssince",
|
|
181
222
|
"max", "min",
|
|
182
223
|
"obv", "accdist", "nvi", "pvi", "pvt", "wad", "wvad", "iii", "vwap"}
|
|
183
224
|
|
|
@@ -40,10 +40,11 @@ from __future__ import annotations
|
|
|
40
40
|
from typing import Any
|
|
41
41
|
|
|
42
42
|
from ..ast_nodes import (
|
|
43
|
-
ASTNode, BinOp, BoolLiteral, ExprStmt, FuncCall, Identifier, IfStmt,
|
|
43
|
+
ASTNode, BinOp, BoolLiteral, ColorLiteral, ExprStmt, FuncCall, Identifier, IfStmt,
|
|
44
44
|
MemberAccess, NaLiteral, NumberLiteral, StringLiteral, Subscript, Ternary,
|
|
45
45
|
SwitchStmt, TupleLiteral, UnaryOp,
|
|
46
46
|
)
|
|
47
|
+
from ..errors import Phase
|
|
47
48
|
from ..symbols import PineType, TypeSpec, method_receiver_type_name
|
|
48
49
|
|
|
49
50
|
# Drawing-objects-as-data type names (spec §4.1). Defined locally — the
|
|
@@ -185,6 +186,10 @@ class TypeHelper:
|
|
|
185
186
|
if left.name == "int" and right.name == "int":
|
|
186
187
|
return TypeSpec.primitive("int")
|
|
187
188
|
return None
|
|
189
|
+
if self._is_color_constructor(value):
|
|
190
|
+
# A packed-ARGB color, as codegen's ``_is_color_value`` reads it:
|
|
191
|
+
# the declaration and the constructor agree on ``int64_t``.
|
|
192
|
+
return TypeSpec.primitive("color")
|
|
188
193
|
spec = self._type_spec_from_expr(value)
|
|
189
194
|
if spec is not None:
|
|
190
195
|
return spec
|
|
@@ -197,6 +202,26 @@ class TypeHelper:
|
|
|
197
202
|
return self._pine_type_to_spec(sym.pine_type)
|
|
198
203
|
return None
|
|
199
204
|
|
|
205
|
+
@classmethod
|
|
206
|
+
def _is_color_constructor(cls, value: ASTNode | None) -> bool:
|
|
207
|
+
"""A color literal, ``color.<name>``, a ``color.new`` / ``rgb`` /
|
|
208
|
+
``from_gradient`` call, or a conditional selecting one."""
|
|
209
|
+
if isinstance(value, ColorLiteral):
|
|
210
|
+
return True
|
|
211
|
+
if isinstance(value, MemberAccess):
|
|
212
|
+
return (isinstance(value.object, Identifier)
|
|
213
|
+
and value.object.name == "color")
|
|
214
|
+
if isinstance(value, FuncCall):
|
|
215
|
+
callee = value.callee
|
|
216
|
+
return (isinstance(callee, MemberAccess)
|
|
217
|
+
and isinstance(callee.object, Identifier)
|
|
218
|
+
and callee.object.name == "color"
|
|
219
|
+
and callee.member in ("new", "rgb", "from_gradient"))
|
|
220
|
+
if isinstance(value, Ternary):
|
|
221
|
+
return (cls._is_color_constructor(value.true_val)
|
|
222
|
+
or cls._is_color_constructor(value.false_val))
|
|
223
|
+
return False
|
|
224
|
+
|
|
200
225
|
@staticmethod
|
|
201
226
|
def _selection_terminal_expr(
|
|
202
227
|
body: list[ASTNode] | None,
|
|
@@ -241,6 +266,13 @@ class TypeHelper:
|
|
|
241
266
|
def _type_spec_from_expr(self, value: ASTNode | None) -> TypeSpec | None:
|
|
242
267
|
if value is None:
|
|
243
268
|
return None
|
|
269
|
+
budget = getattr(self, "_budget", None)
|
|
270
|
+
if budget is not None:
|
|
271
|
+
# A method chain re-infers each receiver more than once, so this
|
|
272
|
+
# recursion can outgrow the visitor checkpoints.
|
|
273
|
+
self._budget_visit_count += 1
|
|
274
|
+
if self._budget_visit_count % 128 == 0:
|
|
275
|
+
budget.check(value.loc, Phase.ANALYZER)
|
|
244
276
|
if isinstance(value, NumberLiteral):
|
|
245
277
|
return TypeSpec.primitive(
|
|
246
278
|
"float" if isinstance(value.value, float) else "int"
|
|
@@ -26,6 +26,28 @@ class ASTNode:
|
|
|
26
26
|
annotations: dict | None = field(default=None, compare=False)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
class ArgOrder:
|
|
30
|
+
"""The written order of a call's positional and keyword arguments.
|
|
31
|
+
|
|
32
|
+
It holds the same nodes as the call's ``args`` and ``kwargs``. It is not a
|
|
33
|
+
list, tuple or dict and has no ``__dict__``, so the generic AST walkers,
|
|
34
|
+
which descend into those, do not visit every argument a second time: the
|
|
35
|
+
second visit doubled the work at each nested call, and ``f(f(f(...)))``
|
|
36
|
+
cost 2**depth.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
__slots__ = ("nodes",)
|
|
40
|
+
|
|
41
|
+
def __init__(self, nodes) -> None:
|
|
42
|
+
self.nodes = tuple(nodes)
|
|
43
|
+
|
|
44
|
+
def __iter__(self):
|
|
45
|
+
return iter(self.nodes)
|
|
46
|
+
|
|
47
|
+
def __len__(self) -> int:
|
|
48
|
+
return len(self.nodes)
|
|
49
|
+
|
|
50
|
+
|
|
29
51
|
# ---------------------------------------------------------------------------
|
|
30
52
|
# Top-level / structural nodes
|
|
31
53
|
# ---------------------------------------------------------------------------
|
|
@@ -44,8 +66,17 @@ class StrategyDecl(ASTNode):
|
|
|
44
66
|
|
|
45
67
|
@dataclass
|
|
46
68
|
class ImportStmt(ASTNode):
|
|
47
|
-
"""import <
|
|
69
|
+
"""``import <user>/<name>/<version> [as <alias>]``.
|
|
70
|
+
|
|
71
|
+
``path`` is ``user/name/version``; ``user``, ``name``, ``version`` and
|
|
72
|
+
``alias`` stay None when the line does not have that form, and ``path``
|
|
73
|
+
is then the line's tokens as written.
|
|
74
|
+
"""
|
|
48
75
|
path: str = ""
|
|
76
|
+
user: str | None = None
|
|
77
|
+
name: str | None = None
|
|
78
|
+
version: int | None = None
|
|
79
|
+
alias: str | None = None
|
|
49
80
|
|
|
50
81
|
|
|
51
82
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Declarations in the script's top-level blocks that cannot share a member.
|
|
2
|
+
|
|
3
|
+
The codegen holds every declaration of the script's top level -- a direct one
|
|
4
|
+
and one inside a top-level ``if`` / loop / ``switch`` block -- in one class
|
|
5
|
+
member per name. Pine scopes a block's declaration to its block, so two blocks
|
|
6
|
+
may declare one name with types one member cannot hold: a counter ``k = 0`` in
|
|
7
|
+
one block and ``k = array.get(names, i)`` (a string) in another, a script of
|
|
8
|
+
the fruit-fly probes' shape, did not compile. The codegen names each
|
|
9
|
+
declaration whose type cannot share the member of the name
|
|
10
|
+
(``CodeGen.block_locals_needing_names``, by ``decl_key``); ``transpile()``
|
|
11
|
+
gives each of them, and every read of it in its block, a name of its own
|
|
12
|
+
(``k__pfblk1``) and runs again (``pineforge_codegen._generate``). Scripts
|
|
13
|
+
whose declarations share their members keep their C++ byte for byte.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from .ast_nodes import (
|
|
19
|
+
ASTNode, ArgOrder, Assignment, ExprStmt, ForInStmt, ForStmt, FuncCall,
|
|
20
|
+
Identifier, IfStmt, Program, SwitchStmt, TupleAssign, VarDecl, WhileStmt,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_BLOCKS = (IfStmt, ForStmt, ForInStmt, WhileStmt, SwitchStmt)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def decl_key(node: VarDecl) -> tuple | None:
|
|
27
|
+
"""A declaration's identity across the passes of one transpile: where
|
|
28
|
+
it is written."""
|
|
29
|
+
loc = node.loc
|
|
30
|
+
return None if loc is None else (loc.file, loc.line, loc.col)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def block_declarations(body: list):
|
|
34
|
+
"""(declaration, in a block) for every declaration the codegen holds in a
|
|
35
|
+
class member of the script's top level, in source order: the direct ones
|
|
36
|
+
and those in its statement blocks (``if`` / loop / ``switch``, nested
|
|
37
|
+
too), as ``CodeGen`` hoists them. An ``if`` or ``switch`` value's block
|
|
38
|
+
keeps its declarations to itself."""
|
|
39
|
+
for stmt in body:
|
|
40
|
+
yield from _declarations(stmt, False)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _declarations(node, nested: bool):
|
|
44
|
+
if isinstance(node, VarDecl):
|
|
45
|
+
yield node, nested
|
|
46
|
+
elif isinstance(node, IfStmt):
|
|
47
|
+
for stmt in (*node.body, *node.else_body):
|
|
48
|
+
yield from _declarations(stmt, True)
|
|
49
|
+
elif isinstance(node, (ForStmt, ForInStmt, WhileStmt)):
|
|
50
|
+
for stmt in node.body:
|
|
51
|
+
yield from _declarations(stmt, True)
|
|
52
|
+
elif isinstance(node, SwitchStmt):
|
|
53
|
+
for _value, body in node.cases:
|
|
54
|
+
for stmt in body:
|
|
55
|
+
yield from _declarations(stmt, True)
|
|
56
|
+
for stmt in node.default_body:
|
|
57
|
+
yield from _declarations(stmt, True)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def rename_block_locals(program: Program, keys: frozenset) -> None:
|
|
61
|
+
"""Give each top-level block declaration ``keys`` names a name of its own,
|
|
62
|
+
and every read of it in its block (a nested block's declaration of the
|
|
63
|
+
same name, or a loop variable of it, shadows it there)."""
|
|
64
|
+
if not keys:
|
|
65
|
+
return
|
|
66
|
+
renamer = _Renamer(program, keys)
|
|
67
|
+
renamer.block(program.body, {})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class _Renamer:
|
|
71
|
+
def __init__(self, program: Program, keys: frozenset) -> None:
|
|
72
|
+
self._keys = keys
|
|
73
|
+
self._taken = _names(program)
|
|
74
|
+
self._next = 0
|
|
75
|
+
|
|
76
|
+
def _alloc(self, name: str) -> str:
|
|
77
|
+
while True:
|
|
78
|
+
self._next += 1
|
|
79
|
+
new = f"{name}__pfblk{self._next}"
|
|
80
|
+
if new not in self._taken:
|
|
81
|
+
self._taken.add(new)
|
|
82
|
+
return new
|
|
83
|
+
|
|
84
|
+
def block(self, stmts: list, visible: dict) -> None:
|
|
85
|
+
names = dict(visible)
|
|
86
|
+
for stmt in stmts:
|
|
87
|
+
self.stmt(stmt, names)
|
|
88
|
+
|
|
89
|
+
def stmt(self, stmt, names: dict) -> None:
|
|
90
|
+
if isinstance(stmt, VarDecl):
|
|
91
|
+
self.expr(stmt.value, names)
|
|
92
|
+
if decl_key(stmt) in self._keys:
|
|
93
|
+
new = self._alloc(stmt.name)
|
|
94
|
+
names[stmt.name] = new
|
|
95
|
+
stmt.name = new
|
|
96
|
+
else:
|
|
97
|
+
names.pop(stmt.name, None)
|
|
98
|
+
elif isinstance(stmt, TupleAssign):
|
|
99
|
+
self.expr(stmt.value, names)
|
|
100
|
+
for bound in stmt.names:
|
|
101
|
+
names.pop(bound, None)
|
|
102
|
+
elif isinstance(stmt, Assignment):
|
|
103
|
+
self.expr(stmt.target, names)
|
|
104
|
+
self.expr(stmt.value, names)
|
|
105
|
+
elif isinstance(stmt, ExprStmt):
|
|
106
|
+
self.expr(stmt.expr, names)
|
|
107
|
+
elif isinstance(stmt, _BLOCKS):
|
|
108
|
+
self.block_node(stmt, names)
|
|
109
|
+
|
|
110
|
+
def block_node(self, node, names: dict) -> None:
|
|
111
|
+
if isinstance(node, IfStmt):
|
|
112
|
+
self.expr(node.condition, names)
|
|
113
|
+
self.block(node.body, names)
|
|
114
|
+
self.block(node.else_body, names)
|
|
115
|
+
elif isinstance(node, ForStmt):
|
|
116
|
+
for part in (node.start, node.end, node.step):
|
|
117
|
+
self.expr(part, names)
|
|
118
|
+
inner = dict(names)
|
|
119
|
+
inner.pop(node.var, None)
|
|
120
|
+
self.block(node.body, inner)
|
|
121
|
+
elif isinstance(node, ForInStmt):
|
|
122
|
+
self.expr(node.iterable, names)
|
|
123
|
+
inner = dict(names)
|
|
124
|
+
for bound in [node.var, *(node.vars or ())]:
|
|
125
|
+
inner.pop(bound, None)
|
|
126
|
+
self.block(node.body, inner)
|
|
127
|
+
elif isinstance(node, WhileStmt):
|
|
128
|
+
self.expr(node.condition, names)
|
|
129
|
+
self.block(node.body, names)
|
|
130
|
+
elif isinstance(node, SwitchStmt):
|
|
131
|
+
self.expr(node.expr, names)
|
|
132
|
+
for value, body in node.cases:
|
|
133
|
+
self.expr(value, names)
|
|
134
|
+
self.block(body, names)
|
|
135
|
+
self.block(node.default_body, names)
|
|
136
|
+
|
|
137
|
+
def expr(self, node, names: dict) -> None:
|
|
138
|
+
if not names or node is None:
|
|
139
|
+
return
|
|
140
|
+
if isinstance(node, list):
|
|
141
|
+
for item in node:
|
|
142
|
+
self.expr(item, names)
|
|
143
|
+
return
|
|
144
|
+
if not isinstance(node, ASTNode):
|
|
145
|
+
return
|
|
146
|
+
if isinstance(node, Identifier):
|
|
147
|
+
node.name = names.get(node.name, node.name)
|
|
148
|
+
return
|
|
149
|
+
if isinstance(node, _BLOCKS):
|
|
150
|
+
self.block_node(node, names)
|
|
151
|
+
return
|
|
152
|
+
for key, value in vars(node).items():
|
|
153
|
+
if key in ("loc", "annotations"):
|
|
154
|
+
continue
|
|
155
|
+
if key == "callee" and isinstance(node, FuncCall) and isinstance(value, Identifier):
|
|
156
|
+
continue # a function's name, not a variable's
|
|
157
|
+
if isinstance(value, dict):
|
|
158
|
+
self.expr(list(value.values()), names)
|
|
159
|
+
elif isinstance(value, (ASTNode, list)):
|
|
160
|
+
self.expr(value, names)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _names(root) -> set[str]:
|
|
164
|
+
"""Every identifier and declared name ``root`` spells."""
|
|
165
|
+
found: set[str] = set()
|
|
166
|
+
stack = [root]
|
|
167
|
+
while stack:
|
|
168
|
+
node = stack.pop()
|
|
169
|
+
if isinstance(node, (list, tuple)):
|
|
170
|
+
stack.extend(node)
|
|
171
|
+
elif isinstance(node, dict):
|
|
172
|
+
stack.extend(node.values())
|
|
173
|
+
elif isinstance(node, ArgOrder):
|
|
174
|
+
continue
|
|
175
|
+
elif isinstance(node, ASTNode):
|
|
176
|
+
for key, value in vars(node).items():
|
|
177
|
+
if key in ("loc", "annotations"):
|
|
178
|
+
continue
|
|
179
|
+
if isinstance(value, str) and key in ("name", "var"):
|
|
180
|
+
found.add(value)
|
|
181
|
+
elif key in ("params", "names", "vars") and isinstance(value, list):
|
|
182
|
+
found.update(v for v in value if isinstance(v, str))
|
|
183
|
+
else:
|
|
184
|
+
stack.append(value)
|
|
185
|
+
return found
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Bind the keyword arguments of ``nz`` and ``fixnan`` to their positions.
|
|
2
|
+
|
|
3
|
+
TradingView names the parameters ``nz(source, replacement)`` and
|
|
4
|
+
``fixnan(source)``, and evaluates a call's arguments in parameter order
|
|
5
|
+
whatever order they are written in (lab tv probes pf-oi-kw-names and
|
|
6
|
+
pf-oi-kw-order: ``nz(replacement = r(), source = s())`` runs ``s`` first).
|
|
7
|
+
The analyzer and the codegen read these calls by position, so a keyword
|
|
8
|
+
form reached them as a call without its source (``nz(source = x)`` and
|
|
9
|
+
``fixnan(source = x)`` raised IndexError) or with its replacement dropped
|
|
10
|
+
(``nz(x, replacement = y)`` read 0). The support checker refuses a call
|
|
11
|
+
whose arguments bind to none of TradingView's signatures; this pass then
|
|
12
|
+
rewrites every call that binds with its arguments in parameter order.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from .ast_nodes import FuncCall, Identifier, Program
|
|
18
|
+
from .limits import iter_ast_nodes
|
|
19
|
+
from . import signatures as sigs
|
|
20
|
+
|
|
21
|
+
POSITIONAL_BUILTINS = ("nz", "fixnan")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def bind_builtin_keywords(program: Program) -> Program:
|
|
25
|
+
for node, _depth in iter_ast_nodes(program):
|
|
26
|
+
if (not isinstance(node, FuncCall) or not node.kwargs
|
|
27
|
+
or not isinstance(node.callee, Identifier)
|
|
28
|
+
or node.callee.name not in POSITIONAL_BUILTINS):
|
|
29
|
+
continue
|
|
30
|
+
names = sigs.BUILTIN_FUNCTIONS[node.callee.name].param_names
|
|
31
|
+
if not set(node.kwargs) <= set(names[len(node.args):]):
|
|
32
|
+
continue # unbound: the support checker refused it
|
|
33
|
+
bound = list(node.args)
|
|
34
|
+
for name in names[len(node.args):]:
|
|
35
|
+
if name not in node.kwargs:
|
|
36
|
+
break
|
|
37
|
+
bound.append(node.kwargs[name])
|
|
38
|
+
if len(bound) != len(node.args) + len(node.kwargs):
|
|
39
|
+
continue # a hole before a later keyword: left as written
|
|
40
|
+
node.args = bound
|
|
41
|
+
node.kwargs = {}
|
|
42
|
+
return program
|