@pineforge/codegen-pyodide 1.0.1 → 1.2.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/LICENSE +245 -0
- package/README.md +4 -0
- package/glue.py +6 -1
- package/package.json +6 -4
- package/pineforge_codegen/__init__.py +25 -9
- package/pineforge_codegen/analyzer/base.py +1 -0
- package/pineforge_codegen/analyzer/call_handlers.py +2 -0
- package/pineforge_codegen/analyzer/contracts.py +3 -0
- package/pineforge_codegen/codegen/base.py +38 -1
- package/pineforge_codegen/codegen/capabilities.py +186 -0
- package/pineforge_codegen/codegen/checked_settings.py +285 -0
- package/pineforge_codegen/codegen/emit_top.py +46 -9
- package/pineforge_codegen/codegen/helpers.py +8 -1
- package/pineforge_codegen/codegen/host_members.py +8 -0
- package/pineforge_codegen/codegen/input.py +36 -3
- package/pineforge_codegen/diagnostic_codes.py +341 -0
- package/pineforge_codegen/diagnostics_catalog.json +631 -0
- package/pineforge_codegen/errors.py +23 -0
- package/pineforge_codegen/external_requests.py +19 -0
- package/pineforge_codegen/request_discovery.py +506 -0
- package/pineforge_codegen-1.2.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/transpile.worker.mjs +6 -1
- package/pineforge_codegen-1.0.1.tar.gz +0 -0
|
@@ -32,6 +32,29 @@ class Diagnostic:
|
|
|
32
32
|
message: str
|
|
33
33
|
hint: str | None = None
|
|
34
34
|
|
|
35
|
+
# The stable code (``PF-E1203`` / ``PF-W0412``) and named arguments of the
|
|
36
|
+
# catalog template the English ``message`` and ``hint`` render from
|
|
37
|
+
# (``diagnostic_codes``, ``diagnostics_catalog.json``). Both are read off
|
|
38
|
+
# the text when first asked for, after the transpile: the text and the
|
|
39
|
+
# C++ never depend on them.
|
|
40
|
+
@property
|
|
41
|
+
def code(self) -> str:
|
|
42
|
+
return self._coded()[0]
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def args(self) -> dict:
|
|
46
|
+
return self._coded()[1]
|
|
47
|
+
|
|
48
|
+
def _coded(self) -> tuple[str, dict]:
|
|
49
|
+
severity = getattr(self.level, "value", self.level)
|
|
50
|
+
key = (severity, self.message, self.hint)
|
|
51
|
+
cached = self.__dict__.get("_pf_coded")
|
|
52
|
+
if cached is None or cached[0] != key:
|
|
53
|
+
from .diagnostic_codes import classify
|
|
54
|
+
cached = (key, classify(severity, self.message, self.hint))
|
|
55
|
+
self.__dict__["_pf_coded"] = cached
|
|
56
|
+
return cached[1]
|
|
57
|
+
|
|
35
58
|
|
|
36
59
|
class CompileError(Exception):
|
|
37
60
|
def __init__(self, diagnostics: list[Diagnostic]):
|
|
@@ -85,6 +85,19 @@ _RECORDED_PARAMS = {
|
|
|
85
85
|
# RequestRef}``: the run stops there only when no data is installed for the
|
|
86
86
|
# request that carries the same ref (``REQUEST_REF_ANNOTATION``).
|
|
87
87
|
UNPINNED_ANNOTATION = "pf_request_unpinned"
|
|
88
|
+
CAPABILITY_UNPINNED_ANNOTATION = "pf_capability_unpinned_requests"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _record_unpinned_capability(program, request):
|
|
92
|
+
notes = program.annotations = dict(program.annotations or {})
|
|
93
|
+
sites = notes.setdefault(CAPABILITY_UNPINNED_ANNOTATION, [])
|
|
94
|
+
sites.append({
|
|
95
|
+
"function": f"request.{request.callee.member}",
|
|
96
|
+
"symbol_node": request.args[0] if request.args else request.kwargs.get("symbol"),
|
|
97
|
+
"tf_node": request.args[1] if len(request.args) > 1 else request.kwargs.get("timeframe"),
|
|
98
|
+
"lookahead_node": request.kwargs.get("lookahead"),
|
|
99
|
+
"gaps_node": request.kwargs.get("gaps"),
|
|
100
|
+
})
|
|
88
101
|
REQUEST_REF_ANNOTATION = "pf_request_ref"
|
|
89
102
|
# On a request whose symbol can select the chart's or another symbol's: it
|
|
90
103
|
# read the chart before it read a feed, and keeps that lowering where no feed
|
|
@@ -785,10 +798,12 @@ def lower_no_data_requests(program: Program) -> Program:
|
|
|
785
798
|
backed.append(node)
|
|
786
799
|
continue
|
|
787
800
|
if lowering == ABSENT_LOWERING:
|
|
801
|
+
_record_unpinned_capability(program, node)
|
|
788
802
|
node.annotations = {**node.annotations, UNPINNED_ANNOTATION: unpinned_message(node)}
|
|
789
803
|
continue
|
|
790
804
|
swaps[id(node)] = _na_of(node, funcs)
|
|
791
805
|
if lowering == "unpinned":
|
|
806
|
+
_record_unpinned_capability(program, node)
|
|
792
807
|
unpinned.append(node)
|
|
793
808
|
if unpinned or backed:
|
|
794
809
|
index = ScriptIndex(program)
|
|
@@ -853,11 +868,15 @@ def unpin_requests(program: Program, reasons: dict[int, str]) -> None:
|
|
|
853
868
|
"results."))
|
|
854
869
|
continue
|
|
855
870
|
lowered = _na_of(request, funcs)
|
|
871
|
+
_record_unpinned_capability(program, request)
|
|
856
872
|
marker = (request.annotations or {}).get(UNPINNED_ANNOTATION)
|
|
857
873
|
if isinstance(marker, dict):
|
|
858
874
|
lowered.annotations = {**(lowered.annotations or {}),
|
|
859
875
|
UNPINNED_ANNOTATION: marker["message"]}
|
|
860
876
|
swaps[request_id] = lowered
|
|
877
|
+
# The request leaves the program; its lowering is the deferred
|
|
878
|
+
# refusal's now (``request_discovery`` lists it so).
|
|
879
|
+
request.annotations = {**(request.annotations or {}), LOWERING_ANNOTATION: "unpinned"}
|
|
861
880
|
warnings.append(pass_warning(
|
|
862
881
|
request,
|
|
863
882
|
f"{spell_call(request)}: no data is pinned for this request; the run stops with "
|
|
@@ -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 "..."
|
|
Binary file
|
package/release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codegen": "1.0
|
|
2
|
+
"codegen": "1.2.0",
|
|
3
3
|
"pyodide": "314.0.0",
|
|
4
4
|
"python": "3.14.0",
|
|
5
5
|
"emscripten": "emscripten_5_0_3",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "7c9654f79d188babd5034d3ebedd2cdd60a8ec2866b70d8ad863a9f06341420e"
|
|
7
7
|
}
|
package/tables.json
CHANGED
package/transpile.worker.mjs
CHANGED
|
@@ -32,6 +32,10 @@ def _diagnostic_entries(diagnostics) -> list:
|
|
|
32
32
|
"col": loc.col if loc else 1,
|
|
33
33
|
"message": message,
|
|
34
34
|
"severity": getattr(d.level, "value", "error"),
|
|
35
|
+
# The stable code and named arguments of the catalog templates the
|
|
36
|
+
# message (and the hint after " — ") render from.
|
|
37
|
+
"code": d.code,
|
|
38
|
+
"args": d.args,
|
|
35
39
|
}
|
|
36
40
|
end_col = getattr(loc, "end_col", None) if loc else None
|
|
37
41
|
if end_col is not None:
|
|
@@ -49,7 +53,8 @@ def transpile_json(source: str) -> str:
|
|
|
49
53
|
# A script that transpiled carries its warnings in the same entry format.
|
|
50
54
|
return json.dumps({"ok": True, "cpp": full["cpp"], "inputs": full["inputs"],
|
|
51
55
|
"strategyParams": full["strategyParams"],
|
|
52
|
-
"diagnostics": _diagnostic_entries(full["diagnostics"])
|
|
56
|
+
"diagnostics": _diagnostic_entries(full["diagnostics"]),
|
|
57
|
+
"requests": full["requests"]})
|
|
53
58
|
`;
|
|
54
59
|
|
|
55
60
|
const post = (m) => self.postMessage(m);
|
|
Binary file
|