@pineforge/codegen-pyodide 1.0.0 → 1.1.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/glue.py +2 -1
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +24 -9
- package/pineforge_codegen/analyzer/base.py +190 -1
- package/pineforge_codegen/analyzer/call_handlers.py +2 -0
- package/pineforge_codegen/analyzer/contracts.py +6 -0
- package/pineforge_codegen/analyzer/types.py +16 -5
- package/pineforge_codegen/ast_nodes.py +7 -0
- package/pineforge_codegen/codegen/base.py +136 -14
- package/pineforge_codegen/codegen/checked_settings.py +272 -0
- package/pineforge_codegen/codegen/collection_history.py +200 -0
- package/pineforge_codegen/codegen/emit_top.py +76 -14
- package/pineforge_codegen/codegen/helpers.py +12 -2
- package/pineforge_codegen/codegen/host_members.py +8 -0
- package/pineforge_codegen/codegen/input.py +36 -3
- package/pineforge_codegen/codegen/security.py +56 -1
- package/pineforge_codegen/codegen/ta.py +65 -1
- package/pineforge_codegen/codegen/types.py +136 -8
- package/pineforge_codegen/codegen/visit_call.py +92 -3
- package/pineforge_codegen/codegen/visit_expr.py +202 -3
- package/pineforge_codegen/codegen/visit_stmt.py +19 -4
- package/pineforge_codegen/collection_history.py +1595 -0
- package/pineforge_codegen/external_requests.py +3 -0
- package/pineforge_codegen/parser.py +19 -4
- package/pineforge_codegen/request_discovery.py +506 -0
- package/pineforge_codegen/support_checker.py +37 -0
- package/pineforge_codegen-1.1.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/transpile.worker.mjs +2 -1
- package/pineforge_codegen-1.0.0.tar.gz +0 -0
|
@@ -858,6 +858,9 @@ def unpin_requests(program: Program, reasons: dict[int, str]) -> None:
|
|
|
858
858
|
lowered.annotations = {**(lowered.annotations or {}),
|
|
859
859
|
UNPINNED_ANNOTATION: marker["message"]}
|
|
860
860
|
swaps[request_id] = lowered
|
|
861
|
+
# The request leaves the program; its lowering is the deferred
|
|
862
|
+
# refusal's now (``request_discovery`` lists it so).
|
|
863
|
+
request.annotations = {**(request.annotations or {}), LOWERING_ANNOTATION: "unpinned"}
|
|
861
864
|
warnings.append(pass_warning(
|
|
862
865
|
request,
|
|
863
866
|
f"{spell_call(request)}: no data is pinned for this request; the run stops with "
|
|
@@ -18,6 +18,7 @@ from .lexer import Token, TokenType
|
|
|
18
18
|
from .errors import CompileError, Diagnostic, Level, Phase, SourceLocation
|
|
19
19
|
from .limits import MAX_NESTING_DEPTH, TimeBudget, limit_error, syntax_children
|
|
20
20
|
from .ast_nodes import (
|
|
21
|
+
HISTORY_MEMBER_ANNOTATION,
|
|
21
22
|
ASTNode, ArgOrder,
|
|
22
23
|
Program, StrategyDecl, ImportStmt,
|
|
23
24
|
VarDecl, Assignment, TupleAssign,
|
|
@@ -1482,6 +1483,11 @@ class Parser:
|
|
|
1482
1483
|
def _parse_postfix(self):
|
|
1483
1484
|
chain_tok = self._current()
|
|
1484
1485
|
expr = self._parse_primary()
|
|
1486
|
+
# Whether ``expr`` is a history reference this chain wrote, not one in
|
|
1487
|
+
# parentheses: TradingView refuses a member straight after it
|
|
1488
|
+
# (``c[1].v``, CE10011; ``b[1].get_top()``, CE10010), which the
|
|
1489
|
+
# support checker reports (HISTORY_MEMBER_ANNOTATION).
|
|
1490
|
+
after_subscript = False
|
|
1485
1491
|
while True:
|
|
1486
1492
|
# Subscript: expr[index]
|
|
1487
1493
|
if self._check(TokenType.LBRACKET):
|
|
@@ -1491,6 +1497,9 @@ class Parser:
|
|
|
1491
1497
|
self._consume(TokenType.RBRACKET)
|
|
1492
1498
|
expr = Subscript(object=expr, index=index)
|
|
1493
1499
|
self._set_loc(expr, start_tok)
|
|
1500
|
+
after_subscript = True
|
|
1501
|
+
self._check_chain(expr, chain_tok)
|
|
1502
|
+
continue
|
|
1494
1503
|
|
|
1495
1504
|
# Member access: expr.member or expr.member(args)
|
|
1496
1505
|
elif self._check(TokenType.DOT):
|
|
@@ -1501,24 +1510,30 @@ class Parser:
|
|
|
1501
1510
|
if self._looks_like_call_template_args():
|
|
1502
1511
|
template_args = self._parse_template_args()
|
|
1503
1512
|
|
|
1513
|
+
annotations = {}
|
|
1514
|
+
if template_args:
|
|
1515
|
+
annotations["template_args"] = template_args
|
|
1516
|
+
if after_subscript:
|
|
1517
|
+
annotations[HISTORY_MEMBER_ANNOTATION] = True
|
|
1504
1518
|
if self._check(TokenType.LPAREN):
|
|
1505
1519
|
# Build callee as MemberAccess, then parse call
|
|
1506
1520
|
callee = MemberAccess(object=expr, member=member_tok.value)
|
|
1507
1521
|
self._set_loc(callee, member_tok)
|
|
1508
|
-
if
|
|
1509
|
-
callee.annotations =
|
|
1522
|
+
if annotations:
|
|
1523
|
+
callee.annotations = annotations
|
|
1510
1524
|
expr = self._parse_call_with_callee(callee)
|
|
1511
1525
|
else:
|
|
1512
1526
|
expr = MemberAccess(object=expr, member=member_tok.value)
|
|
1513
1527
|
self._set_loc(expr, member_tok)
|
|
1514
|
-
if
|
|
1515
|
-
expr.annotations =
|
|
1528
|
+
if annotations:
|
|
1529
|
+
expr.annotations = annotations
|
|
1516
1530
|
|
|
1517
1531
|
# Direct call: expr(args) — needed for identifiers followed by (
|
|
1518
1532
|
elif self._check(TokenType.LPAREN) and self._is_call_position(expr):
|
|
1519
1533
|
expr = self._parse_call_with_callee(expr)
|
|
1520
1534
|
else:
|
|
1521
1535
|
break
|
|
1536
|
+
after_subscript = False
|
|
1522
1537
|
self._check_chain(expr, chain_tok)
|
|
1523
1538
|
return expr
|
|
1524
1539
|
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"""The other symbols' data a script requests, listed before it runs.
|
|
2
|
+
|
|
3
|
+
``transpile_full(...)["requests"]`` names every request site that reads
|
|
4
|
+
another symbol's feed, so a host can fetch the bars a run needs before it
|
|
5
|
+
starts it. A run supplies another symbol's bars as one feed per (symbol
|
|
6
|
+
string, timeframe) and the engine looks a site's feed up byte for byte
|
|
7
|
+
(``codegen/emit_top.py``, ``_emit_foreign_security_registration``), so each
|
|
8
|
+
entry states both the way registration computes them:
|
|
9
|
+
|
|
10
|
+
{"line": 7, "fn": "request.security",
|
|
11
|
+
"symbol": {"kind": "input", "title": "Other symbol", "default": "BINANCE:ETHUSDT"},
|
|
12
|
+
"timeframe": {"kind": "chart"},
|
|
13
|
+
"lookahead": false, "gaps": false, "ignore_invalid_symbol": false}
|
|
14
|
+
|
|
15
|
+
The sites are the registrations the C++ makes: ``request.security`` of
|
|
16
|
+
another symbol that the support checker lowered onto its feed
|
|
17
|
+
(``external_requests.FEED_LOWERING``), one entry per context a helper's call
|
|
18
|
+
paths give it (``security_contexts``), with the feed column a footprint reads
|
|
19
|
+
(``column``). A site whose value reaches display and alert sinks only is
|
|
20
|
+
lowered to ``na`` and reads nothing, and a helper nothing reaches never runs:
|
|
21
|
+
neither is listed. A deferred refusal -- another symbol's request whose value
|
|
22
|
+
can reach a trade but which registration cannot key before the first bar --
|
|
23
|
+
is listed with an ``unresolvable`` symbol: its first read stops the run.
|
|
24
|
+
Every other request reads no feed: the chart's symbol, ``request.*`` of
|
|
25
|
+
fundamentals (recorded series, not bars) and ``request.security_lower_tf`` of
|
|
26
|
+
another symbol (it stops the run where it is evaluated).
|
|
27
|
+
|
|
28
|
+
Names are expanded as registration expands them
|
|
29
|
+
(``_substitute_tf_input_reads``: the expression the C++ renders and the run
|
|
30
|
+
evaluates before the first bar), and a ``value`` is computed only over
|
|
31
|
+
``ScriptIndex.registration_value``'s grammar -- literals, inputs at their
|
|
32
|
+
defaults, ``+ == != and or not``, ternaries and ``ticker.inherit`` /
|
|
33
|
+
``standard`` -- so discovery states a value where the run computes the same
|
|
34
|
+
one from the same expression.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from .ast_nodes import (
|
|
40
|
+
BinOp, BoolLiteral, ExprStmt, FuncCall, Identifier, MemberAccess, NaLiteral,
|
|
41
|
+
NumberLiteral, StringLiteral, Subscript, SwitchStmt, Ternary, UnaryOp,
|
|
42
|
+
)
|
|
43
|
+
from .errors import CompileError, Phase
|
|
44
|
+
from .external_requests import FEED_LOWERING, LOWERING_ANNOTATION, _nodes
|
|
45
|
+
from .pine_spelling import is_input_call, pine_string_literal
|
|
46
|
+
from .security_contexts import _REGISTRATION_INPUTS, ScriptIndex, _reached, ticker_symbol_arg
|
|
47
|
+
|
|
48
|
+
# A deferred refusal's lowering (``support_checker._lower_no_data_request``,
|
|
49
|
+
# ``external_requests.unpin_requests``).
|
|
50
|
+
_UNPINNED_LOWERING = "unpinned"
|
|
51
|
+
# The chart's own timeframe where a fold meets ``timeframe.period``.
|
|
52
|
+
_CHART = object()
|
|
53
|
+
_UNKNOWN = object()
|
|
54
|
+
# A folded value longer than any symbol or timeframe is no key.
|
|
55
|
+
_MAX_VALUE_CHARS = 1024
|
|
56
|
+
# Names a fold follows, one through another, and nodes it visits, before it
|
|
57
|
+
# gives up (the value is then unknown).
|
|
58
|
+
_MAX_FOLD_DEPTH = 256
|
|
59
|
+
_MAX_FOLD_STEPS = 100_000
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _Fold:
|
|
63
|
+
"""One value's fold: each name expanded and folded once."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, gen) -> None:
|
|
66
|
+
self.gen = gen
|
|
67
|
+
self.names: dict[str, object] = {}
|
|
68
|
+
self.steps = 0
|
|
69
|
+
_PRECEDENCE = {"or": 1, "and": 2, "==": 3, "!=": 3, "<": 4, "<=": 4, ">": 4, ">=": 4,
|
|
70
|
+
"+": 5, "-": 5, "*": 6, "/": 6, "%": 6}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def request_sites(program) -> list[FuncCall]:
|
|
74
|
+
"""The ``request.security`` calls the support checker lowered onto
|
|
75
|
+
another symbol's feed or to a deferred refusal, outside the helpers
|
|
76
|
+
nothing reaches: taken before the passes that replace a refusal by its
|
|
77
|
+
``na`` (``lower_no_data_requests``) or turn a feed back into one
|
|
78
|
+
(``unpin_requests``), which mark it so."""
|
|
79
|
+
sites = [node for node in _nodes(program)
|
|
80
|
+
if isinstance(node, FuncCall) and _call_name(node) == ("request", "security")
|
|
81
|
+
and (node.annotations or {}).get(LOWERING_ANNOTATION)
|
|
82
|
+
in (FEED_LOWERING, _UNPINNED_LOWERING)]
|
|
83
|
+
if not sites:
|
|
84
|
+
return sites
|
|
85
|
+
index = ScriptIndex(program)
|
|
86
|
+
reached = _reached(index)
|
|
87
|
+
return [site for site in sites
|
|
88
|
+
if (owner := index.owner.get(id(site))) not in index.funcs or owner in reached]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def discover_requests(gen, ctx, sites: list[FuncCall]) -> list[dict]:
|
|
92
|
+
"""The requests of another symbol's data (module docstring), by line,
|
|
93
|
+
each distinct entry once."""
|
|
94
|
+
entries: list[tuple[tuple, dict]] = []
|
|
95
|
+
calls = {call.sec_id: call for call in ctx.security_calls}
|
|
96
|
+
mutable_reads = set(gen._security_tf_mutable_reads)
|
|
97
|
+
|
|
98
|
+
def add(loc, entry: dict) -> None:
|
|
99
|
+
entry = {"line": loc.line if loc is not None else None, "fn": "request.security",
|
|
100
|
+
**entry}
|
|
101
|
+
where = (loc.line or 0, loc.col or 0) if loc is not None else (0, 0)
|
|
102
|
+
entries.append(((*where, len(entries)), entry))
|
|
103
|
+
|
|
104
|
+
budget = getattr(gen, "_budget", None)
|
|
105
|
+
foreign = [info for info in gen._security_eval_info if info.get("foreign")]
|
|
106
|
+
helpers: set[str] = set()
|
|
107
|
+
unreached: set[str] = set()
|
|
108
|
+
if any(getattr(calls.get(info["sec_id"]), "containing_func", "") for info in foreign):
|
|
109
|
+
# A helper nothing reaches never runs: its registration is never read.
|
|
110
|
+
index = ScriptIndex(ctx.ast)
|
|
111
|
+
helpers = set(index.funcs)
|
|
112
|
+
unreached = helpers - _reached(index)
|
|
113
|
+
try:
|
|
114
|
+
for info in foreign:
|
|
115
|
+
helper = getattr(calls.get(info["sec_id"]), "containing_func", "")
|
|
116
|
+
if helper in helpers and helper in unreached:
|
|
117
|
+
continue
|
|
118
|
+
# Past the time budget the rest are listed as written, unexpanded:
|
|
119
|
+
# discovery never fails a script that transpiled.
|
|
120
|
+
expand = _within(budget)
|
|
121
|
+
entry = {
|
|
122
|
+
"symbol": _guarded(_symbol, gen, info["symbol_node"], expand=expand),
|
|
123
|
+
"timeframe": _guarded(_registered_timeframe, gen, info, expand=expand),
|
|
124
|
+
"lookahead": bool(info.get("lookahead_on")),
|
|
125
|
+
"gaps": bool(info.get("gaps_on")),
|
|
126
|
+
"ignore_invalid_symbol": _guarded(
|
|
127
|
+
_flag, gen, info.get("ignore_invalid_node"), expand=expand),
|
|
128
|
+
}
|
|
129
|
+
column = gen._security_footprint_column(info["sec_id"])
|
|
130
|
+
if column:
|
|
131
|
+
entry["column"] = column
|
|
132
|
+
add(getattr(calls.get(info["sec_id"]), "loc", None), entry)
|
|
133
|
+
for site in sites:
|
|
134
|
+
if (site.annotations or {}).get(LOWERING_ANNOTATION) != _UNPINNED_LOWERING:
|
|
135
|
+
continue
|
|
136
|
+
# The C++ never registered it, so nothing expands its names here:
|
|
137
|
+
# its arguments are listed as written.
|
|
138
|
+
symbol, timeframe = _request_args(site)
|
|
139
|
+
gaps, lookahead = _gaps_lookahead(site)
|
|
140
|
+
ignore = site.kwargs.get("ignore_invalid_symbol")
|
|
141
|
+
add(site.loc, {
|
|
142
|
+
"symbol": {"kind": "unresolvable", "expr": _spell(symbol)},
|
|
143
|
+
"timeframe": _guarded(_timeframe, gen, timeframe, expand=False),
|
|
144
|
+
"lookahead": _barmerge(lookahead, "lookahead_on"),
|
|
145
|
+
"gaps": _barmerge(gaps, "gaps_on"),
|
|
146
|
+
"ignore_invalid_symbol": (
|
|
147
|
+
False if ignore is None else
|
|
148
|
+
ignore.value if isinstance(ignore, BoolLiteral) else None),
|
|
149
|
+
})
|
|
150
|
+
finally:
|
|
151
|
+
# Expanding a name registers a reassigned global's read with the
|
|
152
|
+
# first-bar replay; generation is over, so leave its record as it was.
|
|
153
|
+
gen._security_tf_mutable_reads = mutable_reads
|
|
154
|
+
out: list[dict] = []
|
|
155
|
+
for _, entry in sorted(entries, key=lambda item: item[0]):
|
|
156
|
+
if entry not in out:
|
|
157
|
+
out.append(entry)
|
|
158
|
+
return out
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# Symbol and timeframe
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def _within(budget) -> bool:
|
|
166
|
+
"""Whether the transpile's time budget still holds."""
|
|
167
|
+
if budget is None:
|
|
168
|
+
return True
|
|
169
|
+
try:
|
|
170
|
+
budget.check(phase=Phase.CODEGEN)
|
|
171
|
+
except CompileError:
|
|
172
|
+
return False
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _guarded(classify, gen, node, **options):
|
|
177
|
+
"""``classify(gen, node)``; a shape it does not know is listed as
|
|
178
|
+
computed, with no value: discovery never fails a script that
|
|
179
|
+
transpiled."""
|
|
180
|
+
try:
|
|
181
|
+
return classify(gen, node, **options)
|
|
182
|
+
except Exception: # noqa: BLE001
|
|
183
|
+
if isinstance(node, dict):
|
|
184
|
+
node = node.get("tf_node")
|
|
185
|
+
return None if classify is _flag else {"kind": "computed", "expr": _spell(node)}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _symbol(gen, node, expand: bool = True) -> dict:
|
|
189
|
+
"""``literal`` / ``input`` / ``computed`` as registration computes the
|
|
190
|
+
symbol string the run keys its feed on."""
|
|
191
|
+
resolved = _passthrough(_expand(gen, node) if expand else node)
|
|
192
|
+
if isinstance(resolved, StringLiteral):
|
|
193
|
+
return {"kind": "literal", "value": resolved.value}
|
|
194
|
+
if _is_registered_input(resolved):
|
|
195
|
+
return _input(gen, resolved)
|
|
196
|
+
if not expand:
|
|
197
|
+
return {"kind": "computed", "expr": _spell(node)}
|
|
198
|
+
return _computed(gen, node, resolved, timeframe=False)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _registered_timeframe(gen, info: dict, expand: bool = True) -> dict:
|
|
202
|
+
"""The timeframe a feed site registers with (``_resolve_security_tf``'s
|
|
203
|
+
``tf`` / ``tf_expr``, ``configure_security_evaluators``). ``input_tf_``
|
|
204
|
+
is the chart's: a run reading another symbol's feed takes the chart's
|
|
205
|
+
bars unaggregated (the engine refuses it otherwise)."""
|
|
206
|
+
tf, tf_expr = info.get("tf"), info.get("tf_expr")
|
|
207
|
+
if tf:
|
|
208
|
+
return {"kind": "literal", "value": canonical_timeframe(tf)}
|
|
209
|
+
if tf == "" or tf_expr in ("input_tf_", "script_tf_"):
|
|
210
|
+
return {"kind": "chart"}
|
|
211
|
+
return _timeframe(gen, info.get("tf_node"), expand=expand)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _timeframe(gen, node, expand: bool = True) -> dict:
|
|
215
|
+
"""A timeframe registration expands (``expand``), or as written."""
|
|
216
|
+
if node is None:
|
|
217
|
+
return {"kind": "chart"}
|
|
218
|
+
resolved = _expand(gen, node) if expand else node
|
|
219
|
+
if isinstance(resolved, StringLiteral):
|
|
220
|
+
if resolved.value == "":
|
|
221
|
+
return {"kind": "chart"}
|
|
222
|
+
return {"kind": "literal", "value": canonical_timeframe(resolved.value)}
|
|
223
|
+
if _is_timeframe_period(resolved):
|
|
224
|
+
return {"kind": "chart"}
|
|
225
|
+
if _is_registered_input(resolved, timeframe=True):
|
|
226
|
+
return _input(gen, resolved)
|
|
227
|
+
if not expand:
|
|
228
|
+
return {"kind": "computed", "expr": _spell(node)}
|
|
229
|
+
return _computed(gen, node, resolved, timeframe=True)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def canonical_timeframe(tf: str) -> str:
|
|
233
|
+
"""The engine's spelling of a feed's timeframe
|
|
234
|
+
(``canonical_symbol_timeframe``): Pine's bare ``D`` / ``W`` / ``M`` /
|
|
235
|
+
``S`` folded to ``1D`` / ``1W`` / ``1M`` / ``1S``, any other text kept
|
|
236
|
+
as it is (whole minutes ``"240"``, ``<n>D|W|M|S``; a feed matches no
|
|
237
|
+
other spelling)."""
|
|
238
|
+
return "1" + tf if tf in ("D", "W", "M", "S") else tf
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _input(gen, call: FuncCall) -> dict:
|
|
242
|
+
default = _input_default(gen, call)
|
|
243
|
+
return {"kind": "input", "title": gen._get_input_title(call),
|
|
244
|
+
"default": None if default is _UNKNOWN else default}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _computed(gen, node, resolved, *, timeframe: bool) -> dict:
|
|
248
|
+
out = {"kind": "computed", "expr": _spell(node)}
|
|
249
|
+
value = _fold(_Fold(gen), resolved)
|
|
250
|
+
if value is _CHART and timeframe:
|
|
251
|
+
out["value"] = ""
|
|
252
|
+
elif isinstance(value, str):
|
|
253
|
+
out["value"] = canonical_timeframe(value) if timeframe and value else value
|
|
254
|
+
titles: list[str] = []
|
|
255
|
+
for call in _input_calls(gen, resolved):
|
|
256
|
+
title = gen._get_input_title(call)
|
|
257
|
+
if title not in titles:
|
|
258
|
+
titles.append(title)
|
|
259
|
+
if titles:
|
|
260
|
+
out["inputs"] = titles
|
|
261
|
+
return out
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _flag(gen, node, expand: bool = True) -> bool | None:
|
|
265
|
+
"""``ignore_invalid_symbol`` as registration reads it: false when left
|
|
266
|
+
out, else its value at the inputs' defaults (None when unknown)."""
|
|
267
|
+
if node is None:
|
|
268
|
+
return False
|
|
269
|
+
if not expand:
|
|
270
|
+
return node.value if isinstance(node, BoolLiteral) else None
|
|
271
|
+
value = _fold(_Fold(gen), _expand(gen, node))
|
|
272
|
+
return value if isinstance(value, bool) else None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _expand(gen, node):
|
|
276
|
+
"""``node`` with its names expanded as registration expands them."""
|
|
277
|
+
return gen._substitute_tf_input_reads(node, set())
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _passthrough(node):
|
|
281
|
+
"""``ticker.inherit`` / ``ticker.standard`` render their symbol argument
|
|
282
|
+
unchanged (``visit_call``)."""
|
|
283
|
+
while (isinstance(node, FuncCall) and _call_name(node) in (
|
|
284
|
+
("ticker", "inherit"), ("ticker", "standard"))):
|
|
285
|
+
symbol = ticker_symbol_arg(node)
|
|
286
|
+
if symbol is None:
|
|
287
|
+
break
|
|
288
|
+
node = symbol
|
|
289
|
+
return node
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _is_registered_input(node, timeframe: bool = False) -> bool:
|
|
293
|
+
"""An input whose value registration reads: ``input.symbol`` /
|
|
294
|
+
``string`` (``timeframe`` for a timeframe), or ``input()`` of a string."""
|
|
295
|
+
if not is_input_call(node):
|
|
296
|
+
return False
|
|
297
|
+
ns, name = _call_name(node)
|
|
298
|
+
if ns is None:
|
|
299
|
+
default = node.args[0] if node.args else node.kwargs.get("defval")
|
|
300
|
+
return isinstance(default, StringLiteral)
|
|
301
|
+
return name in (_REGISTRATION_INPUTS - {"bool"}) or (timeframe and name == "timeframe")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _input_default(gen, call: FuncCall):
|
|
305
|
+
default = gen._get_input_default(call)
|
|
306
|
+
if isinstance(default, (StringLiteral, BoolLiteral, NumberLiteral)):
|
|
307
|
+
return default.value
|
|
308
|
+
return _UNKNOWN
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _input_calls(gen, node) -> list[FuncCall]:
|
|
312
|
+
"""The input calls ``node`` reads, through the names registration
|
|
313
|
+
expands (a ``switch``'s too), in source order."""
|
|
314
|
+
found: dict[int, FuncCall] = {}
|
|
315
|
+
seen: set[str] = set()
|
|
316
|
+
pending = [node]
|
|
317
|
+
while pending:
|
|
318
|
+
for item in _nodes(pending.pop()):
|
|
319
|
+
if is_input_call(item):
|
|
320
|
+
found.setdefault(id(item), item)
|
|
321
|
+
elif isinstance(item, Identifier) and item.name not in seen:
|
|
322
|
+
seen.add(item.name)
|
|
323
|
+
expanded = _expand(gen, item)
|
|
324
|
+
if expanded is not item:
|
|
325
|
+
pending.append(expanded)
|
|
326
|
+
return sorted(found.values(), key=lambda call: (call.loc.line, call.loc.col)
|
|
327
|
+
if call.loc is not None else (0, 0))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _fold(state: _Fold, node, depth: int = 0):
|
|
331
|
+
"""The value at the inputs' defaults of an expression in
|
|
332
|
+
``ScriptIndex.registration_value``'s grammar (and, for a timeframe, a
|
|
333
|
+
``switch`` of single-expression arms, as registration renders it), else
|
|
334
|
+
``_UNKNOWN``. The chart's own symbol strings are the chart's (unknown
|
|
335
|
+
here); ``timeframe.period`` is ``_CHART``."""
|
|
336
|
+
state.steps += 1
|
|
337
|
+
if depth > _MAX_FOLD_DEPTH or state.steps > _MAX_FOLD_STEPS:
|
|
338
|
+
return _UNKNOWN
|
|
339
|
+
depth += 1
|
|
340
|
+
if isinstance(node, (StringLiteral, BoolLiteral)):
|
|
341
|
+
return node.value
|
|
342
|
+
if isinstance(node, NumberLiteral):
|
|
343
|
+
return node.value
|
|
344
|
+
if _is_timeframe_period(node):
|
|
345
|
+
return _CHART
|
|
346
|
+
if isinstance(node, Identifier):
|
|
347
|
+
# A name registration expands (a switch's subject: the expansion
|
|
348
|
+
# leaves a switch as written); one it keeps has no value here.
|
|
349
|
+
if node.name not in state.names:
|
|
350
|
+
state.names[node.name] = _UNKNOWN # a name read inside its own value
|
|
351
|
+
expanded = _expand(state.gen, node)
|
|
352
|
+
state.names[node.name] = (_UNKNOWN if expanded is node
|
|
353
|
+
else _fold(state, expanded, depth))
|
|
354
|
+
return state.names[node.name]
|
|
355
|
+
if isinstance(node, SwitchStmt):
|
|
356
|
+
return _fold_switch(state, node, depth)
|
|
357
|
+
if isinstance(node, FuncCall):
|
|
358
|
+
if is_input_call(node):
|
|
359
|
+
ns, name = _call_name(node)
|
|
360
|
+
if ns is None or name in _REGISTRATION_INPUTS or name == "timeframe":
|
|
361
|
+
return _input_default(state.gen, node)
|
|
362
|
+
return _UNKNOWN
|
|
363
|
+
passed = _passthrough(node)
|
|
364
|
+
return _UNKNOWN if passed is node else _fold(state, passed, depth)
|
|
365
|
+
if isinstance(node, Ternary):
|
|
366
|
+
condition = _fold(state, node.condition, depth)
|
|
367
|
+
if not isinstance(condition, bool):
|
|
368
|
+
return _UNKNOWN
|
|
369
|
+
return _fold(state, node.true_val if condition else node.false_val, depth)
|
|
370
|
+
if isinstance(node, UnaryOp):
|
|
371
|
+
operand = _fold(state, node.operand, depth)
|
|
372
|
+
return (not operand) if node.op == "not" and isinstance(operand, bool) else _UNKNOWN
|
|
373
|
+
if isinstance(node, BinOp) and node.op in ("+", "==", "!=", "and", "or"):
|
|
374
|
+
left = _fold(state, node.left, depth)
|
|
375
|
+
if left is _UNKNOWN or left is _CHART:
|
|
376
|
+
return _UNKNOWN
|
|
377
|
+
right = _fold(state, node.right, depth)
|
|
378
|
+
if right is _UNKNOWN or right is _CHART:
|
|
379
|
+
return _UNKNOWN
|
|
380
|
+
if node.op == "+":
|
|
381
|
+
if not (isinstance(left, str) and isinstance(right, str)
|
|
382
|
+
and len(left) + len(right) <= _MAX_VALUE_CHARS):
|
|
383
|
+
return _UNKNOWN
|
|
384
|
+
return left + right
|
|
385
|
+
if node.op in ("and", "or"):
|
|
386
|
+
if not (isinstance(left, bool) and isinstance(right, bool)):
|
|
387
|
+
return _UNKNOWN
|
|
388
|
+
return (left and right) if node.op == "and" else (left or right)
|
|
389
|
+
if type(left) is not type(right):
|
|
390
|
+
return _UNKNOWN
|
|
391
|
+
return (left == right) if node.op == "==" else (left != right)
|
|
392
|
+
return _UNKNOWN
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _fold_switch(state: _Fold, node: SwitchStmt, depth: int):
|
|
396
|
+
"""A timeframe ``switch`` as registration renders it
|
|
397
|
+
(``_security_tf_switch_runtime_expr``): the first arm whose value equals
|
|
398
|
+
the subject (or whose condition holds), else the default arm, else the
|
|
399
|
+
chart's timeframe; every arm one expression."""
|
|
400
|
+
subject = None
|
|
401
|
+
if node.expr is not None:
|
|
402
|
+
subject = _fold(state, node.expr, depth)
|
|
403
|
+
if subject is _UNKNOWN or subject is _CHART:
|
|
404
|
+
return _UNKNOWN
|
|
405
|
+
|
|
406
|
+
def arm(body):
|
|
407
|
+
if len(body) != 1 or not isinstance(body[0], ExprStmt) or isinstance(
|
|
408
|
+
body[0].expr, NaLiteral):
|
|
409
|
+
return _UNKNOWN
|
|
410
|
+
return _fold(state, body[0].expr, depth)
|
|
411
|
+
|
|
412
|
+
default = node.default_body or None
|
|
413
|
+
for condition, body in node.cases:
|
|
414
|
+
if condition is None:
|
|
415
|
+
default = default or body
|
|
416
|
+
continue
|
|
417
|
+
value = _fold(state, condition, depth)
|
|
418
|
+
if value is _UNKNOWN or value is _CHART:
|
|
419
|
+
return _UNKNOWN
|
|
420
|
+
if (value is True if node.expr is None
|
|
421
|
+
else type(value) is type(subject) and value == subject):
|
|
422
|
+
return arm(body)
|
|
423
|
+
return arm(default) if default else _CHART
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# ---------------------------------------------------------------------------
|
|
427
|
+
# Call shapes and spelling
|
|
428
|
+
# ---------------------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
def _call_name(node: FuncCall) -> tuple[str | None, str | None]:
|
|
431
|
+
callee = node.callee
|
|
432
|
+
if isinstance(callee, Identifier):
|
|
433
|
+
return None, callee.name
|
|
434
|
+
if isinstance(callee, MemberAccess) and isinstance(callee.object, Identifier):
|
|
435
|
+
return callee.object.name, callee.member
|
|
436
|
+
return None, None
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _is_timeframe_period(node) -> bool:
|
|
440
|
+
"""``timeframe.period`` or ``timeframe.main_period``: the chart's."""
|
|
441
|
+
return (isinstance(node, MemberAccess) and node.member in ("period", "main_period")
|
|
442
|
+
and isinstance(node.object, Identifier) and node.object.name == "timeframe")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _request_args(node: FuncCall):
|
|
446
|
+
symbol = node.args[0] if node.args else node.kwargs.get("symbol")
|
|
447
|
+
timeframe = node.args[1] if len(node.args) > 1 else node.kwargs.get("timeframe")
|
|
448
|
+
return symbol, timeframe
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _gaps_lookahead(node: FuncCall):
|
|
452
|
+
gaps = node.kwargs.get("gaps")
|
|
453
|
+
if gaps is None and len(node.args) > 3:
|
|
454
|
+
gaps = node.args[3]
|
|
455
|
+
lookahead = node.kwargs.get("lookahead")
|
|
456
|
+
if lookahead is None and len(node.args) > 4:
|
|
457
|
+
lookahead = node.args[4]
|
|
458
|
+
return gaps, lookahead
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _barmerge(node, on: str) -> bool | None:
|
|
462
|
+
"""A ``barmerge.*`` constant's flag; left out is off; None when computed."""
|
|
463
|
+
if node is None:
|
|
464
|
+
return False
|
|
465
|
+
if (isinstance(node, MemberAccess) and isinstance(node.object, Identifier)
|
|
466
|
+
and node.object.name == "barmerge"):
|
|
467
|
+
return node.member == on
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _spell(node, parent: int = 0, right: bool = False) -> str:
|
|
472
|
+
"""``node`` spelled as Pine source (display only)."""
|
|
473
|
+
if node is None:
|
|
474
|
+
return ""
|
|
475
|
+
if isinstance(node, StringLiteral):
|
|
476
|
+
return pine_string_literal(node.value)
|
|
477
|
+
if isinstance(node, BoolLiteral):
|
|
478
|
+
return "true" if node.value else "false"
|
|
479
|
+
if isinstance(node, NumberLiteral):
|
|
480
|
+
return str(node.value)
|
|
481
|
+
if isinstance(node, NaLiteral):
|
|
482
|
+
return "na"
|
|
483
|
+
if isinstance(node, Identifier):
|
|
484
|
+
return node.name
|
|
485
|
+
if isinstance(node, MemberAccess):
|
|
486
|
+
return f"{_spell(node.object, 9)}.{node.member}"
|
|
487
|
+
if isinstance(node, FuncCall):
|
|
488
|
+
args = [_spell(a) for a in node.args]
|
|
489
|
+
args += [f"{key} = {_spell(value)}" for key, value in node.kwargs.items()]
|
|
490
|
+
return f"{_spell(node.callee, 9)}({', '.join(args)})"
|
|
491
|
+
if isinstance(node, Subscript):
|
|
492
|
+
return f"{_spell(node.object, 9)}[{_spell(node.index)}]"
|
|
493
|
+
if isinstance(node, UnaryOp):
|
|
494
|
+
text = f"not {_spell(node.operand, 7)}" if node.op == "not" else (
|
|
495
|
+
f"{node.op}{_spell(node.operand, 7)}")
|
|
496
|
+
return f"({text})" if parent > 7 else text
|
|
497
|
+
if isinstance(node, BinOp):
|
|
498
|
+
level = _PRECEDENCE.get(node.op, 5)
|
|
499
|
+
text = (f"{_spell(node.left, level)} {node.op} "
|
|
500
|
+
f"{_spell(node.right, level, right=True)}")
|
|
501
|
+
return f"({text})" if level < parent or (right and level == parent) else text
|
|
502
|
+
if isinstance(node, Ternary):
|
|
503
|
+
text = (f"{_spell(node.condition, 1)} ? {_spell(node.true_val)} : "
|
|
504
|
+
f"{_spell(node.false_val)}")
|
|
505
|
+
return f"({text})" if parent else text
|
|
506
|
+
return "..."
|
|
@@ -36,6 +36,7 @@ import re
|
|
|
36
36
|
from typing import Callable
|
|
37
37
|
|
|
38
38
|
from .ast_nodes import (
|
|
39
|
+
HISTORY_MEMBER_ANNOTATION,
|
|
39
40
|
ASTNode,
|
|
40
41
|
Program, StrategyDecl, ImportStmt, TypeField,
|
|
41
42
|
VarDecl, Assignment, TupleAssign,
|
|
@@ -1482,6 +1483,40 @@ class SupportChecker:
|
|
|
1482
1483
|
)
|
|
1483
1484
|
self._visit_children(node)
|
|
1484
1485
|
|
|
1486
|
+
def _refuse_history_member(self, node: MemberAccess) -> None:
|
|
1487
|
+
"""A field read or a method call written straight after the
|
|
1488
|
+
history-referencing operator: TradingView refuses ``c[1].v``
|
|
1489
|
+
("User variable identifiers should not contain '.'", CE10011) and
|
|
1490
|
+
``b[1].get_top()`` (CE10010), and takes the history reference in
|
|
1491
|
+
parentheses, ``(c[1]).v``, which reads the field of the object the
|
|
1492
|
+
variable held (fixtures/udt_history_tv)."""
|
|
1493
|
+
history = node.object
|
|
1494
|
+
receiver = "x[k]"
|
|
1495
|
+
if (isinstance(history, Subscript)
|
|
1496
|
+
and isinstance(history.object, Identifier)
|
|
1497
|
+
and isinstance(history.index, (NumberLiteral, Identifier))):
|
|
1498
|
+
index = (history.index.name if isinstance(history.index, Identifier)
|
|
1499
|
+
else history.index.value)
|
|
1500
|
+
receiver = f"{history.object.name}[{index}]"
|
|
1501
|
+
if id(node) in self._callee_node_ids:
|
|
1502
|
+
self._err(
|
|
1503
|
+
node,
|
|
1504
|
+
f"{receiver}.{node.member}(): TradingView refuses a method "
|
|
1505
|
+
"call straight after the history-referencing operator "
|
|
1506
|
+
"(CE10010; CE10011 after an array's or a matrix's history).",
|
|
1507
|
+
hint=f"Call the method on the history reference in "
|
|
1508
|
+
f"parentheses: ({receiver}).{node.member}().",
|
|
1509
|
+
)
|
|
1510
|
+
return
|
|
1511
|
+
self._err(
|
|
1512
|
+
node,
|
|
1513
|
+
f"{receiver}.{node.member}: TradingView refuses a field read "
|
|
1514
|
+
"straight after the history-referencing operator (CE10011: "
|
|
1515
|
+
"\"User variable identifiers should not contain '.'\").",
|
|
1516
|
+
hint=f"Read the field of the history reference in parentheses: "
|
|
1517
|
+
f"({receiver}).{node.member}.",
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1485
1520
|
def _visit_FuncCall(self, node: FuncCall) -> None:
|
|
1486
1521
|
ns, name = _qualified_name(node.callee)
|
|
1487
1522
|
|
|
@@ -1976,6 +2011,8 @@ class SupportChecker:
|
|
|
1976
2011
|
self._visit(node.false_val)
|
|
1977
2012
|
|
|
1978
2013
|
def _visit_MemberAccess(self, node: MemberAccess) -> None:
|
|
2014
|
+
if (node.annotations or {}).get(HISTORY_MEMBER_ANNOTATION):
|
|
2015
|
+
self._refuse_history_member(node)
|
|
1979
2016
|
chain = _resolve_member_chain(node)
|
|
1980
2017
|
if (
|
|
1981
2018
|
chain is not None
|
|
Binary file
|
package/release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codegen": "1.
|
|
2
|
+
"codegen": "1.1.0",
|
|
3
3
|
"pyodide": "314.0.0",
|
|
4
4
|
"python": "3.14.0",
|
|
5
5
|
"emscripten": "emscripten_5_0_3",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "37b1ca85a2006f6f07d52074a7180187536047bfbb857681978a372b1d7f9bef"
|
|
7
7
|
}
|
package/tables.json
CHANGED
package/transpile.worker.mjs
CHANGED
|
@@ -49,7 +49,8 @@ def transpile_json(source: str) -> str:
|
|
|
49
49
|
# A script that transpiled carries its warnings in the same entry format.
|
|
50
50
|
return json.dumps({"ok": True, "cpp": full["cpp"], "inputs": full["inputs"],
|
|
51
51
|
"strategyParams": full["strategyParams"],
|
|
52
|
-
"diagnostics": _diagnostic_entries(full["diagnostics"])
|
|
52
|
+
"diagnostics": _diagnostic_entries(full["diagnostics"]),
|
|
53
|
+
"requests": full["requests"]})
|
|
53
54
|
`;
|
|
54
55
|
|
|
55
56
|
const post = (m) => self.postMessage(m);
|
|
Binary file
|