@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
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
"""Pine v5 rules inside the bodies of an inlined v5 library.
|
|
2
|
+
|
|
3
|
+
A library keeps its own ``//@version``: TradingView compiles it on its own,
|
|
4
|
+
and a v6 script can import a v5 one (``jdehorty/MLExtensions/2`` and
|
|
5
|
+
``jdehorty/KernelFunctions/2`` are v5). Where v5 and v6 read the same code
|
|
6
|
+
differently, a v5 library body must be lowered with v5's rules.
|
|
7
|
+
``V5_RULES`` enumerates every difference TradingView's migration guide to v6
|
|
8
|
+
lists ("Here are the changes that affect v5 scripts") and what PineForge does
|
|
9
|
+
with it inside a v5 library body: lower it with v5's rule here or in the
|
|
10
|
+
codegen (``annotations["pine_version"] == 5`` on the inlined function or
|
|
11
|
+
method), refuse the construct by the library's name, or nothing because the
|
|
12
|
+
difference cannot reach a library body's lowering.
|
|
13
|
+
|
|
14
|
+
Each implemented rule is pinned by TradingView's tape of a synthetic v5
|
|
15
|
+
script (``tests/fixtures/xsym_lib_tv``), replayed through a synthetic v5
|
|
16
|
+
library holding the same code (``tests/test_e2e_library_v5.py``).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
|
|
23
|
+
from .ast_nodes import (
|
|
24
|
+
ArgOrder, ASTNode, Assignment, BinOp, BoolLiteral, ColorLiteral, ExprStmt,
|
|
25
|
+
ForInStmt, ForStmt, FuncCall, FuncDef, Identifier, IfStmt, MemberAccess,
|
|
26
|
+
MethodDef, NaLiteral, NumberLiteral, StringLiteral, Subscript, SwitchStmt,
|
|
27
|
+
Ternary, TupleAssign, TupleLiteral, TypeDecl, UnaryOp, VarDecl, WhileStmt,
|
|
28
|
+
)
|
|
29
|
+
from .errors import CompileError, Diagnostic, Level, Phase, SourceLocation
|
|
30
|
+
from .library_modules import LibraryModule
|
|
31
|
+
from . import signatures as sigs
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class V5Rule:
|
|
36
|
+
change: str # the migration guide's wording
|
|
37
|
+
disposition: str # "implemented" | "refused" | "not applicable"
|
|
38
|
+
how: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# The migration guide's list, in its order (pine-script-docs
|
|
42
|
+
# migration-guides/to-pine-version-6, read 2026-09-28).
|
|
43
|
+
V5_RULES: dict[str, V5Rule] = {
|
|
44
|
+
"implicit-bool-cast": V5Rule(
|
|
45
|
+
"Values of the int and float types are no longer implicitly cast to bool.",
|
|
46
|
+
"implemented",
|
|
47
|
+
"the codegen's truthiness helper reads na and 0 as false in every "
|
|
48
|
+
"bool context, which is v5's implicit cast"),
|
|
49
|
+
"bool-na": V5Rule(
|
|
50
|
+
"Boolean values can no longer be na, and the na(), nz(), and fixnan() "
|
|
51
|
+
"functions no longer accept bool arguments.",
|
|
52
|
+
"refused",
|
|
53
|
+
"a v5 bool na (a comparison with na, an na literal, a bool's history "
|
|
54
|
+
"before the first bar, an if without else) reads as false wherever v5 "
|
|
55
|
+
"casts it (conditions, not, and, or, ?:) and across the library "
|
|
56
|
+
"boundary (TradingView's v6 caller reads it false); na(), nz(), "
|
|
57
|
+
"fixnan() and str.tostring/str.format of a bool, and == / != with a "
|
|
58
|
+
"bool operand, observe the third state and are refused"),
|
|
59
|
+
"lazy-and-or": V5Rule(
|
|
60
|
+
"The and and or operators now evaluate conditions lazily.",
|
|
61
|
+
"implemented",
|
|
62
|
+
"the codegen evaluates both operands of and/or, left then right, "
|
|
63
|
+
"every time the expression is evaluated; ?: stays lazy (v5 too)"),
|
|
64
|
+
"dynamic-requests": V5Rule(
|
|
65
|
+
"All request.*() functions can now execute dynamically.",
|
|
66
|
+
"refused",
|
|
67
|
+
"a request.*() call in a v5 library body"),
|
|
68
|
+
"const-int-division": V5Rule(
|
|
69
|
+
"Division of two const int values can now return a fractional value.",
|
|
70
|
+
"implemented",
|
|
71
|
+
"a division of two const ints is truncated toward zero, int(a / b); "
|
|
72
|
+
"a const is a literal, a const declaration or a variable never "
|
|
73
|
+
"reassigned with a const value, arithmetic, ?:, int(), math.abs/"
|
|
74
|
+
"max/min/round/floor/ceil and str.length of consts; a typed "
|
|
75
|
+
"parameter, a reassigned variable, an input, nz() or any series is "
|
|
76
|
+
"not, and a division whose constness depends on an untyped "
|
|
77
|
+
"parameter (v5 types it per call) is refused"),
|
|
78
|
+
"when-parameter": V5Rule(
|
|
79
|
+
"The when parameter is removed from all applicable strategy.*() functions.",
|
|
80
|
+
"not applicable", "a library cannot call strategy.*()"),
|
|
81
|
+
"default-margin": V5Rule(
|
|
82
|
+
"The default long and short margin percentage for strategies is now 100.",
|
|
83
|
+
"not applicable", "a strategy() declaration, never a library's"),
|
|
84
|
+
"excess-orders": V5Rule(
|
|
85
|
+
"Strategies now trim the oldest orders in their results instead of "
|
|
86
|
+
"raising an error when they exceed the 9000 trade limit.",
|
|
87
|
+
"not applicable", "the importing strategy's order book"),
|
|
88
|
+
"exit-parameter-pairs": V5Rule(
|
|
89
|
+
"The strategy.exit() command no longer ignores relative parameters "
|
|
90
|
+
"... when the call also includes arguments for the related absolute "
|
|
91
|
+
"parameters.",
|
|
92
|
+
"not applicable", "a library cannot call strategy.exit()"),
|
|
93
|
+
"literal-and-field-history": V5Rule(
|
|
94
|
+
"The history-referencing operator [] can no longer reference the "
|
|
95
|
+
"history of literal values or fields of user-defined types directly.",
|
|
96
|
+
"refused",
|
|
97
|
+
"[] on a literal, a built-in constant or a user-defined type's field "
|
|
98
|
+
"in a v5 library body"),
|
|
99
|
+
"repeated-parameters": V5Rule(
|
|
100
|
+
"Function calls can no longer include more than one argument for the "
|
|
101
|
+
"same parameter.",
|
|
102
|
+
"refused",
|
|
103
|
+
"the parser refuses a keyword argument given twice (v5 kept the "
|
|
104
|
+
"first), located in the library"),
|
|
105
|
+
"series-offset": V5Rule(
|
|
106
|
+
"The offset parameter of plot() and other functions no longer accepts "
|
|
107
|
+
"series values.",
|
|
108
|
+
"not applicable", "plot() and its kin run at the global scope; a "
|
|
109
|
+
"library's global code is never inlined"),
|
|
110
|
+
"unique-type-na": V5Rule(
|
|
111
|
+
"na values are no longer allowed in place of built-in constants of "
|
|
112
|
+
"unique types.",
|
|
113
|
+
"not applicable", "those parameters style plots and drawings, which "
|
|
114
|
+
"the codegen drops"),
|
|
115
|
+
"timeframe-period-multiplier": V5Rule(
|
|
116
|
+
'The value of timeframe.period now always includes a multiplier '
|
|
117
|
+
'(e.g., "1D" instead of "D").',
|
|
118
|
+
"implemented",
|
|
119
|
+
'timeframe.period reads "D", "W" and "M" for a 1D, 1W and 1M chart'),
|
|
120
|
+
"negative-array-index": V5Rule(
|
|
121
|
+
"Some array.*() functions now accept negative index arguments.",
|
|
122
|
+
"implemented",
|
|
123
|
+
"array get/set/insert/remove stop the run on a negative index "
|
|
124
|
+
"(TradingView's v5 runtime error), where v6 counts from the end"),
|
|
125
|
+
"mutable-const": V5Rule(
|
|
126
|
+
"Some mutable variables are no longer erroneously marked as const.",
|
|
127
|
+
"refused",
|
|
128
|
+
"a reassigned variable passed where a ta.*() or math.sum() length is "
|
|
129
|
+
"(v5 runs it at its first value); a reassigned variable is not const "
|
|
130
|
+
"in a division (TradingView's tape: fractional)"),
|
|
131
|
+
"transp-parameter": V5Rule(
|
|
132
|
+
"The transp parameter is removed from all applicable functions.",
|
|
133
|
+
"not applicable", "plot-style functions, global scope only"),
|
|
134
|
+
"default-colors": V5Rule(
|
|
135
|
+
"Some default colors and color constants have updated values.",
|
|
136
|
+
"implemented",
|
|
137
|
+
"color.red, color.teal and color.yellow keep their v5 values "
|
|
138
|
+
"(#FF5252, #00897B, #FFEB3B); label.new's default text color is a "
|
|
139
|
+
"drawing style the codegen drops"),
|
|
140
|
+
"dynamic-for-boundary": V5Rule(
|
|
141
|
+
"The for loop statement now evaluates its end boundary dynamically "
|
|
142
|
+
"before every iteration.",
|
|
143
|
+
"implemented",
|
|
144
|
+
"the codegen evaluates a for loop's `to` once, before the first "
|
|
145
|
+
"iteration"),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_V5_COLORS = {"red": "#FF5252", "teal": "#00897B", "yellow": "#FFEB3B"}
|
|
149
|
+
|
|
150
|
+
# Built-in namespaces whose members are constants: ``[]`` on one is history
|
|
151
|
+
# of a constant (refused under v5).
|
|
152
|
+
_CONSTANT_NAMESPACES = frozenset({
|
|
153
|
+
"color", "shape", "location", "size", "position", "text", "xloc", "yloc",
|
|
154
|
+
"extend", "plot", "hline", "display", "format", "order", "currency",
|
|
155
|
+
"dayofweek", "barmerge", "font", "scale", "alert", "adjustment",
|
|
156
|
+
"backadjustment", "settlement_as_close", "label", "line", "strategy",
|
|
157
|
+
})
|
|
158
|
+
_NAMESPACES = _CONSTANT_NAMESPACES | frozenset({
|
|
159
|
+
"ta", "math", "str", "array", "map", "matrix", "input", "request",
|
|
160
|
+
"syminfo", "timeframe", "barstate", "session", "chart", "table",
|
|
161
|
+
"linefill", "polyline", "log", "runtime", "ticker", "dividends",
|
|
162
|
+
"earnings", "splits", "footprint", "volume_row", "box",
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
# What the codegen lowers by v5's rules only inside a v5 function or method
|
|
166
|
+
# (``CodeGen._pine_v5_body``): a top-level declaration or a type's field
|
|
167
|
+
# default, emitted with the script's code, refuses it.
|
|
168
|
+
_BODY_ONLY_OPS = frozenset({"and", "or", "==", "!="})
|
|
169
|
+
_BODY_ONLY_CALLS = frozenset({"na", "nz", "fixnan", "string"})
|
|
170
|
+
_BODY_ONLY_MEMBER_CALLS = frozenset({
|
|
171
|
+
("str", "tostring"), ("str", "format"),
|
|
172
|
+
("array", "get"), ("array", "set"), ("array", "insert"), ("array", "remove"),
|
|
173
|
+
})
|
|
174
|
+
_BODY_ONLY_METHODS = frozenset({"get", "set", "insert", "remove"})
|
|
175
|
+
|
|
176
|
+
_INT_BUILTIN_VARS = frozenset({
|
|
177
|
+
"bar_index", "last_bar_index", "time", "time_close", "timenow",
|
|
178
|
+
"time_tradingday", "year", "month", "weekofyear", "dayofmonth",
|
|
179
|
+
"dayofweek", "hour", "minute", "second",
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
# Qualifier lattice of the v5 const-int analysis.
|
|
183
|
+
C, N, U = "const", "nonconst", "unknown"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _combine(*qs: str) -> str:
|
|
187
|
+
if N in qs:
|
|
188
|
+
return N
|
|
189
|
+
if U in qs:
|
|
190
|
+
return U
|
|
191
|
+
return C
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _kind_of_hint(hint) -> str:
|
|
195
|
+
if not isinstance(hint, str):
|
|
196
|
+
return "?"
|
|
197
|
+
return {"int": "int", "float": "float", "bool": "bool",
|
|
198
|
+
"string": "string"}.get(hint, "?")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class _Refusal(Exception):
|
|
202
|
+
def __init__(self, node: ASTNode | None, message: str) -> None:
|
|
203
|
+
super().__init__(message)
|
|
204
|
+
self.node = node
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class _Callable:
|
|
208
|
+
"""What the const-int analysis knows of one inlined body."""
|
|
209
|
+
|
|
210
|
+
def __init__(self, node, module: "_V5Module") -> None:
|
|
211
|
+
self.module = module
|
|
212
|
+
hints = list((node.annotations or {}).get("param_type_hints") or ())
|
|
213
|
+
self.params = {p: (hints[i] if i < len(hints) else None)
|
|
214
|
+
for i, p in enumerate(node.params)}
|
|
215
|
+
self.decls: dict[str, list[VarDecl]] = {}
|
|
216
|
+
self.bound: set[str] = set() # tuple, loop and other bindings
|
|
217
|
+
self.reassigned: set[str] = set()
|
|
218
|
+
for stmt in _statements(node.body):
|
|
219
|
+
if isinstance(stmt, VarDecl):
|
|
220
|
+
self.decls.setdefault(stmt.name, []).append(stmt)
|
|
221
|
+
elif isinstance(stmt, TupleAssign):
|
|
222
|
+
self.bound.update(n for n in stmt.names if n != "_")
|
|
223
|
+
elif isinstance(stmt, Assignment) and isinstance(stmt.target, Identifier):
|
|
224
|
+
self.reassigned.add(stmt.target.name)
|
|
225
|
+
elif isinstance(stmt, ForStmt) and stmt.var:
|
|
226
|
+
self.bound.add(stmt.var)
|
|
227
|
+
elif isinstance(stmt, ForInStmt):
|
|
228
|
+
self.bound.update(v for v in [stmt.var, *(stmt.vars or [])] if v)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class _V5Module:
|
|
232
|
+
def __init__(self, lib: LibraryModule, definitions: list, names: dict[str, str]) -> None:
|
|
233
|
+
self.lib = lib
|
|
234
|
+
self.definitions = definitions
|
|
235
|
+
self.globals: dict[str, VarDecl] = {}
|
|
236
|
+
self.reassigned: set[str] = set()
|
|
237
|
+
for stmt in definitions:
|
|
238
|
+
if isinstance(stmt, VarDecl):
|
|
239
|
+
self.globals[stmt.name] = stmt
|
|
240
|
+
for stmt in lib.program.body:
|
|
241
|
+
if isinstance(stmt, Assignment) and isinstance(stmt.target, Identifier):
|
|
242
|
+
original = stmt.target.name
|
|
243
|
+
self.reassigned.add(names.get(original, original))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _statements(body):
|
|
247
|
+
"""Every statement of a body, nested blocks included."""
|
|
248
|
+
stack = list(reversed(body or []))
|
|
249
|
+
while stack:
|
|
250
|
+
stmt = stack.pop()
|
|
251
|
+
yield stmt
|
|
252
|
+
blocks = []
|
|
253
|
+
if isinstance(stmt, IfStmt):
|
|
254
|
+
blocks = [stmt.body, stmt.else_body]
|
|
255
|
+
elif isinstance(stmt, (ForStmt, ForInStmt, WhileStmt)):
|
|
256
|
+
blocks = [stmt.body]
|
|
257
|
+
elif isinstance(stmt, SwitchStmt):
|
|
258
|
+
blocks = [b for _v, b in stmt.cases] + [stmt.default_body]
|
|
259
|
+
for value in (getattr(stmt, "value", None), getattr(stmt, "expr", None)):
|
|
260
|
+
if isinstance(value, (IfStmt, SwitchStmt, ForStmt, WhileStmt)):
|
|
261
|
+
stack.append(value)
|
|
262
|
+
for block in reversed(blocks):
|
|
263
|
+
stack.extend(reversed(block or []))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _walk(expr):
|
|
267
|
+
"""``expr`` and every AST node below it."""
|
|
268
|
+
stack = [expr]
|
|
269
|
+
while stack:
|
|
270
|
+
node = stack.pop()
|
|
271
|
+
if not isinstance(node, ASTNode):
|
|
272
|
+
continue
|
|
273
|
+
yield node
|
|
274
|
+
for key, value in vars(node).items():
|
|
275
|
+
if key in ("loc", "annotations"):
|
|
276
|
+
continue
|
|
277
|
+
if isinstance(value, ASTNode):
|
|
278
|
+
stack.append(value)
|
|
279
|
+
elif isinstance(value, list):
|
|
280
|
+
stack.extend(v for v in value if isinstance(v, ASTNode))
|
|
281
|
+
elif isinstance(value, dict):
|
|
282
|
+
stack.extend(v for v in value.values() if isinstance(v, ASTNode))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class _Lowering:
|
|
286
|
+
"""The v5 lowering of one inlined definition."""
|
|
287
|
+
|
|
288
|
+
def __init__(self, module: _V5Module, node) -> None:
|
|
289
|
+
self.module = module
|
|
290
|
+
self.node = node
|
|
291
|
+
self.fn = _Callable(node, module) if isinstance(node, (FuncDef, MethodDef)) else None
|
|
292
|
+
|
|
293
|
+
# -- the const-int analysis ------------------------------------------
|
|
294
|
+
|
|
295
|
+
def kind(self, expr) -> tuple[str, str]:
|
|
296
|
+
"""(kind, qualifier) of ``expr``: kind int/float/bool/string/na/?,
|
|
297
|
+
qualifier const/nonconst/unknown under v5's rules (TradingView's tape
|
|
298
|
+
of xc_v5_lib, ``tests/fixtures/xsym_lib_tv``)."""
|
|
299
|
+
if isinstance(expr, NumberLiteral):
|
|
300
|
+
return ("int" if isinstance(expr.value, int) else "float"), C
|
|
301
|
+
if isinstance(expr, NaLiteral):
|
|
302
|
+
return "na", C
|
|
303
|
+
if isinstance(expr, BoolLiteral):
|
|
304
|
+
return "bool", C
|
|
305
|
+
if isinstance(expr, StringLiteral):
|
|
306
|
+
return "string", C
|
|
307
|
+
if isinstance(expr, ColorLiteral):
|
|
308
|
+
return "color", C
|
|
309
|
+
if isinstance(expr, Identifier):
|
|
310
|
+
return self._identifier(expr.name)
|
|
311
|
+
if isinstance(expr, UnaryOp):
|
|
312
|
+
k, q = self.kind(expr.operand)
|
|
313
|
+
return ("bool", q) if expr.op == "not" else (k, q)
|
|
314
|
+
if isinstance(expr, BinOp):
|
|
315
|
+
if expr.op in ("and", "or", "==", "!=", "<", ">", "<=", ">="):
|
|
316
|
+
return "bool", _combine(self.kind(expr.left)[1], self.kind(expr.right)[1])
|
|
317
|
+
(kl, ql), (kr, qr) = self.kind(expr.left), self.kind(expr.right)
|
|
318
|
+
q = _combine(ql, qr)
|
|
319
|
+
if expr.op == "/":
|
|
320
|
+
if kl == kr == "int":
|
|
321
|
+
return ("int", C) if q == C else ("float", q)
|
|
322
|
+
return ("float" if "float" in (kl, kr) else "?"), q
|
|
323
|
+
if kl == kr and kl in ("int", "float", "string"):
|
|
324
|
+
return kl, q
|
|
325
|
+
if {kl, kr} <= {"int", "float"}:
|
|
326
|
+
return "float", q
|
|
327
|
+
return "?", q
|
|
328
|
+
if isinstance(expr, Ternary):
|
|
329
|
+
(_kc, qc), (ka, qa), (kb, qb) = (self.kind(expr.condition),
|
|
330
|
+
self.kind(expr.true_val),
|
|
331
|
+
self.kind(expr.false_val))
|
|
332
|
+
q = _combine(qc, qa, qb)
|
|
333
|
+
if ka == kb:
|
|
334
|
+
return ka, q
|
|
335
|
+
if {ka, kb} <= {"int", "float"}:
|
|
336
|
+
return "float", q
|
|
337
|
+
return "?", q
|
|
338
|
+
if isinstance(expr, FuncCall):
|
|
339
|
+
return self._call_kind(expr)
|
|
340
|
+
if isinstance(expr, Subscript):
|
|
341
|
+
return self.kind(expr.object)[0], N
|
|
342
|
+
if isinstance(expr, MemberAccess):
|
|
343
|
+
if isinstance(expr.object, Identifier) and expr.object.name == "color":
|
|
344
|
+
return "color", C
|
|
345
|
+
return "?", N
|
|
346
|
+
return "?", U
|
|
347
|
+
|
|
348
|
+
def _identifier(self, name: str) -> tuple[str, str]:
|
|
349
|
+
fn = self.fn
|
|
350
|
+
if fn is not None and name in fn.params:
|
|
351
|
+
hint = fn.params[name]
|
|
352
|
+
# A typed parameter is series (or simple) whatever the argument
|
|
353
|
+
# (TradingView: xc_v5_lib's typedDiv(5, 5, 5) divides its int,
|
|
354
|
+
# simple int and series int parameters by 2 as 2.5); an untyped
|
|
355
|
+
# one takes its call's qualifier (a probe outside this
|
|
356
|
+
# repository: 2 for the argument 5, 0.5 for bar_index on bar 1).
|
|
357
|
+
return (_kind_of_hint(hint), N) if hint else ("?", U)
|
|
358
|
+
if fn is not None and (name in fn.decls or name in fn.bound):
|
|
359
|
+
if name in fn.bound or name in fn.reassigned:
|
|
360
|
+
return self._decl_kind(fn.decls.get(name)), N
|
|
361
|
+
if len(fn.decls[name]) != 1:
|
|
362
|
+
# Declared in sibling blocks: each is its own variable.
|
|
363
|
+
return self._decl_kind(fn.decls[name]), U
|
|
364
|
+
decl = fn.decls[name][0]
|
|
365
|
+
k, q = self.kind(decl.value)
|
|
366
|
+
return (_kind_of_hint(decl.type_hint) if decl.type_hint else k), q
|
|
367
|
+
glob = self.module.globals.get(name)
|
|
368
|
+
if glob is not None:
|
|
369
|
+
if name in self.module.reassigned:
|
|
370
|
+
return self._decl_kind([glob]), N
|
|
371
|
+
k, q = self.kind(glob.value)
|
|
372
|
+
return (_kind_of_hint(glob.type_hint) if glob.type_hint else k), q
|
|
373
|
+
if name in _INT_BUILTIN_VARS:
|
|
374
|
+
return "int", N
|
|
375
|
+
if name == "na":
|
|
376
|
+
return "na", C
|
|
377
|
+
return "?", N
|
|
378
|
+
|
|
379
|
+
def _decl_kind(self, decls) -> str:
|
|
380
|
+
for decl in decls or ():
|
|
381
|
+
if decl.type_hint:
|
|
382
|
+
return _kind_of_hint(decl.type_hint)
|
|
383
|
+
return "?"
|
|
384
|
+
|
|
385
|
+
def _call_kind(self, call: FuncCall) -> tuple[str, str]:
|
|
386
|
+
callee = call.callee
|
|
387
|
+
args = list(call.args) + list(call.kwargs.values())
|
|
388
|
+
kinds = [self.kind(a) for a in args]
|
|
389
|
+
q = _combine(*(k[1] for k in kinds)) if kinds else C
|
|
390
|
+
if isinstance(callee, Identifier):
|
|
391
|
+
name = callee.name
|
|
392
|
+
if name == "int":
|
|
393
|
+
return "int", q
|
|
394
|
+
if name == "float":
|
|
395
|
+
return "float", q
|
|
396
|
+
if name == "bool":
|
|
397
|
+
return "bool", q
|
|
398
|
+
if name == "string":
|
|
399
|
+
return "string", q
|
|
400
|
+
if name == "nz":
|
|
401
|
+
# TradingView: nz(7) / 2 divides as 3.5.
|
|
402
|
+
return (kinds[0][0] if kinds else "?"), N
|
|
403
|
+
if name == "na":
|
|
404
|
+
return "bool", q
|
|
405
|
+
# A user function's result: series unless every argument is a
|
|
406
|
+
# const, where v5's per-call typing is unknown.
|
|
407
|
+
return "?", (U if q == C else N)
|
|
408
|
+
if isinstance(callee, MemberAccess) and isinstance(callee.object, Identifier):
|
|
409
|
+
ns, member = callee.object.name, callee.member
|
|
410
|
+
if ns == "math":
|
|
411
|
+
if member in ("abs",) and kinds:
|
|
412
|
+
return kinds[0][0], q
|
|
413
|
+
if member in ("max", "min"):
|
|
414
|
+
kk = {k for k, _ in kinds}
|
|
415
|
+
return ("int" if kk == {"int"} else "float"), q
|
|
416
|
+
if member in ("round", "floor", "ceil") and len(args) == 1:
|
|
417
|
+
return "int", q
|
|
418
|
+
return "float", q
|
|
419
|
+
if ns == "str" and member == "length":
|
|
420
|
+
return "int", q
|
|
421
|
+
if ns in _NAMESPACES:
|
|
422
|
+
return "?", N
|
|
423
|
+
return "?", (U if q == C else N)
|
|
424
|
+
|
|
425
|
+
# -- rewriting --------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
def lower(self) -> None:
|
|
428
|
+
node = self.node
|
|
429
|
+
if isinstance(node, (FuncDef, MethodDef)):
|
|
430
|
+
self._block(node.body)
|
|
431
|
+
notes = node.annotations or {}
|
|
432
|
+
node.annotations = dict(notes, param_defaults=[
|
|
433
|
+
None if d is None else self.expr(d)
|
|
434
|
+
for d in notes.get("param_defaults") or ()])
|
|
435
|
+
elif isinstance(node, (VarDecl, TupleAssign)):
|
|
436
|
+
node.value = self.expr(node.value)
|
|
437
|
+
self._check_outside_body(node.value, "a top-level declaration")
|
|
438
|
+
elif isinstance(node, TypeDecl):
|
|
439
|
+
for fld in node.fields:
|
|
440
|
+
if fld.default is not None:
|
|
441
|
+
fld.default = self.expr(fld.default)
|
|
442
|
+
self._check_outside_body(fld.default, "a field's default")
|
|
443
|
+
|
|
444
|
+
def _check_outside_body(self, expr, where: str) -> None:
|
|
445
|
+
"""Refuse in ``expr``, emitted outside the library's functions, what
|
|
446
|
+
the codegen lowers by v5's rules only inside them."""
|
|
447
|
+
for node in _walk(expr):
|
|
448
|
+
what = None
|
|
449
|
+
if isinstance(node, BinOp) and node.op in _BODY_ONLY_OPS:
|
|
450
|
+
what = f"'{node.op}'"
|
|
451
|
+
elif isinstance(node, FuncCall):
|
|
452
|
+
callee = node.callee
|
|
453
|
+
if isinstance(callee, Identifier) and callee.name in _BODY_ONLY_CALLS:
|
|
454
|
+
what = f"{callee.name}()"
|
|
455
|
+
elif isinstance(callee, MemberAccess):
|
|
456
|
+
ns = callee.object.name if isinstance(callee.object, Identifier) else None
|
|
457
|
+
if (ns, callee.member) in _BODY_ONLY_MEMBER_CALLS:
|
|
458
|
+
what = f"{ns}.{callee.member}()"
|
|
459
|
+
elif ns not in _NAMESPACES and callee.member in _BODY_ONLY_METHODS:
|
|
460
|
+
what = f"the method {callee.member}()"
|
|
461
|
+
if what is not None:
|
|
462
|
+
raise _Refusal(node, (
|
|
463
|
+
f"{what} in {where}: v5 reads it differently from v6, and "
|
|
464
|
+
"PineForge applies v5's rule only inside the library's "
|
|
465
|
+
"functions and methods"))
|
|
466
|
+
|
|
467
|
+
def _block(self, stmts) -> None:
|
|
468
|
+
for i, stmt in enumerate(stmts or []):
|
|
469
|
+
stmts[i] = self.stmt(stmt)
|
|
470
|
+
|
|
471
|
+
def stmt(self, stmt):
|
|
472
|
+
if isinstance(stmt, VarDecl):
|
|
473
|
+
stmt.value = self.expr(stmt.value)
|
|
474
|
+
elif isinstance(stmt, TupleAssign):
|
|
475
|
+
stmt.value = self.expr(stmt.value)
|
|
476
|
+
elif isinstance(stmt, Assignment):
|
|
477
|
+
stmt.target = self.expr(stmt.target)
|
|
478
|
+
stmt.value = self.expr(stmt.value)
|
|
479
|
+
elif isinstance(stmt, ExprStmt):
|
|
480
|
+
stmt.expr = self.expr(stmt.expr)
|
|
481
|
+
elif isinstance(stmt, (IfStmt, ForStmt, ForInStmt, WhileStmt, SwitchStmt)):
|
|
482
|
+
self._block_node(stmt)
|
|
483
|
+
return stmt
|
|
484
|
+
|
|
485
|
+
def _block_node(self, node):
|
|
486
|
+
if isinstance(node, IfStmt):
|
|
487
|
+
node.condition = self.expr(node.condition)
|
|
488
|
+
self._block(node.body)
|
|
489
|
+
self._block(node.else_body)
|
|
490
|
+
elif isinstance(node, ForStmt):
|
|
491
|
+
node.start, node.end = self.expr(node.start), self.expr(node.end)
|
|
492
|
+
if node.step is not None:
|
|
493
|
+
node.step = self.expr(node.step)
|
|
494
|
+
self._block(node.body)
|
|
495
|
+
elif isinstance(node, ForInStmt):
|
|
496
|
+
node.iterable = self.expr(node.iterable)
|
|
497
|
+
self._block(node.body)
|
|
498
|
+
elif isinstance(node, WhileStmt):
|
|
499
|
+
node.condition = self.expr(node.condition)
|
|
500
|
+
self._block(node.body)
|
|
501
|
+
elif isinstance(node, SwitchStmt):
|
|
502
|
+
if node.expr is not None:
|
|
503
|
+
node.expr = self.expr(node.expr)
|
|
504
|
+
node.cases = [(None if v is None else self.expr(v), b) for v, b in node.cases]
|
|
505
|
+
for _v, body in node.cases:
|
|
506
|
+
self._block(body)
|
|
507
|
+
self._block(node.default_body)
|
|
508
|
+
return node
|
|
509
|
+
|
|
510
|
+
def expr(self, node):
|
|
511
|
+
if node is None or not isinstance(node, ASTNode):
|
|
512
|
+
return node
|
|
513
|
+
if isinstance(node, (IfStmt, ForStmt, ForInStmt, WhileStmt, SwitchStmt)):
|
|
514
|
+
return self._block_node(node)
|
|
515
|
+
if isinstance(node, MemberAccess):
|
|
516
|
+
if isinstance(node.object, Identifier):
|
|
517
|
+
ns, member = node.object.name, node.member
|
|
518
|
+
if ns == "color" and member in _V5_COLORS:
|
|
519
|
+
return self._at(ColorLiteral(value=_V5_COLORS[member]), node)
|
|
520
|
+
if ns == "timeframe" and member == "period":
|
|
521
|
+
return self._v5_timeframe_period(node)
|
|
522
|
+
node.object = self.expr(node.object)
|
|
523
|
+
return node
|
|
524
|
+
if isinstance(node, Subscript):
|
|
525
|
+
self._check_history(node)
|
|
526
|
+
node.object = self.expr(node.object)
|
|
527
|
+
node.index = self.expr(node.index)
|
|
528
|
+
return node
|
|
529
|
+
if isinstance(node, FuncCall):
|
|
530
|
+
self._check_call(node)
|
|
531
|
+
node.callee = self.expr(node.callee) if not isinstance(node.callee, Identifier) else node.callee
|
|
532
|
+
replaced = {}
|
|
533
|
+
args = []
|
|
534
|
+
for a in node.args:
|
|
535
|
+
new = self.expr(a)
|
|
536
|
+
if new is not a:
|
|
537
|
+
replaced[id(a)] = new
|
|
538
|
+
args.append(new)
|
|
539
|
+
kwargs = {}
|
|
540
|
+
for k, a in node.kwargs.items():
|
|
541
|
+
new = self.expr(a)
|
|
542
|
+
if new is not a:
|
|
543
|
+
replaced[id(a)] = new
|
|
544
|
+
kwargs[k] = new
|
|
545
|
+
node.args, node.kwargs = args, kwargs
|
|
546
|
+
if replaced:
|
|
547
|
+
notes = node.annotations or {}
|
|
548
|
+
order = notes.get("call_arg_order")
|
|
549
|
+
if isinstance(order, ArgOrder):
|
|
550
|
+
node.annotations = dict(notes, call_arg_order=ArgOrder(
|
|
551
|
+
replaced.get(id(n), n) for n in order))
|
|
552
|
+
return node
|
|
553
|
+
if isinstance(node, BinOp):
|
|
554
|
+
node.left = self.expr(node.left)
|
|
555
|
+
node.right = self.expr(node.right)
|
|
556
|
+
if node.op == "/":
|
|
557
|
+
return self._division(node)
|
|
558
|
+
return node
|
|
559
|
+
if isinstance(node, UnaryOp):
|
|
560
|
+
node.operand = self.expr(node.operand)
|
|
561
|
+
return node
|
|
562
|
+
if isinstance(node, Ternary):
|
|
563
|
+
node.condition = self.expr(node.condition)
|
|
564
|
+
node.true_val = self.expr(node.true_val)
|
|
565
|
+
node.false_val = self.expr(node.false_val)
|
|
566
|
+
return node
|
|
567
|
+
if isinstance(node, TupleLiteral):
|
|
568
|
+
node.elements = [self.expr(e) for e in node.elements]
|
|
569
|
+
return node
|
|
570
|
+
return node
|
|
571
|
+
|
|
572
|
+
@staticmethod
|
|
573
|
+
def _at(new: ASTNode, like: ASTNode) -> ASTNode:
|
|
574
|
+
new.loc = like.loc
|
|
575
|
+
return new
|
|
576
|
+
|
|
577
|
+
def _division(self, node: BinOp):
|
|
578
|
+
(kl, ql), (kr, qr) = self.kind(node.left), self.kind(node.right)
|
|
579
|
+
if "float" in (kl, kr) or "na" in (kl, kr) or N in (ql, qr):
|
|
580
|
+
return node
|
|
581
|
+
if kl == kr == "int" and ql == qr == C:
|
|
582
|
+
# v5 divides two const ints as integers, truncating toward zero
|
|
583
|
+
# (TradingView: -7 / 2 = -3, 7 / -2 = -3); int() of v6's
|
|
584
|
+
# fractional quotient is that.
|
|
585
|
+
callee = self._at(Identifier(name="int"), node)
|
|
586
|
+
call = self._at(FuncCall(callee=callee, args=[node], kwargs={}), node)
|
|
587
|
+
return call
|
|
588
|
+
raise _Refusal(node, (
|
|
589
|
+
"v5 divides two const ints as integers, and whether both operands "
|
|
590
|
+
"of this division are const depends on its call (an untyped "
|
|
591
|
+
"parameter takes its argument's qualifier in v5); PineForge does "
|
|
592
|
+
"not specialize a v5 function per call"))
|
|
593
|
+
|
|
594
|
+
def _v5_timeframe_period(self, node: MemberAccess):
|
|
595
|
+
"""``timeframe.period`` as v5 spells it: no multiplier of 1 for a
|
|
596
|
+
day, week or month (TradingView: "D" on a 1D chart)."""
|
|
597
|
+
def period():
|
|
598
|
+
return self._at(MemberAccess(
|
|
599
|
+
object=self._at(Identifier(name="timeframe"), node), member="period"), node)
|
|
600
|
+
|
|
601
|
+
result = period()
|
|
602
|
+
for v6, v5 in (("1M", "M"), ("1W", "W"), ("1D", "D")):
|
|
603
|
+
cond = self._at(BinOp(left=period(), op="==",
|
|
604
|
+
right=self._at(StringLiteral(value=v6), node)), node)
|
|
605
|
+
result = self._at(Ternary(condition=cond,
|
|
606
|
+
true_val=self._at(StringLiteral(value=v5), node),
|
|
607
|
+
false_val=result), node)
|
|
608
|
+
return result
|
|
609
|
+
|
|
610
|
+
def _check_history(self, node: Subscript) -> None:
|
|
611
|
+
obj = node.object
|
|
612
|
+
if isinstance(obj, (NumberLiteral, StringLiteral, BoolLiteral, ColorLiteral, NaLiteral)):
|
|
613
|
+
raise _Refusal(node, "[] on a literal reads its history, which v5 "
|
|
614
|
+
"allows and PineForge does not implement")
|
|
615
|
+
if isinstance(obj, MemberAccess) and isinstance(obj.object, Identifier):
|
|
616
|
+
if obj.object.name in _CONSTANT_NAMESPACES:
|
|
617
|
+
raise _Refusal(node, "[] on a built-in constant reads its history, "
|
|
618
|
+
"which v5 allows and PineForge does not implement")
|
|
619
|
+
if obj.object.name not in _NAMESPACES:
|
|
620
|
+
raise _Refusal(node, "[] on a user-defined type's field reads its "
|
|
621
|
+
"history directly, which v5 allows and "
|
|
622
|
+
"PineForge does not implement")
|
|
623
|
+
elif isinstance(obj, MemberAccess):
|
|
624
|
+
raise _Refusal(node, "[] on a user-defined type's field reads its "
|
|
625
|
+
"history directly, which v5 allows and PineForge "
|
|
626
|
+
"does not implement")
|
|
627
|
+
|
|
628
|
+
def _check_call(self, call: FuncCall) -> None:
|
|
629
|
+
callee = call.callee
|
|
630
|
+
if not (isinstance(callee, MemberAccess) and isinstance(callee.object, Identifier)):
|
|
631
|
+
return
|
|
632
|
+
ns, member = callee.object.name, callee.member
|
|
633
|
+
if ns == "request":
|
|
634
|
+
raise _Refusal(call, f"request.{member}() in a v5 library runs under "
|
|
635
|
+
"v5's non-dynamic request rules, which PineForge "
|
|
636
|
+
"does not implement")
|
|
637
|
+
length_params: set[int | str] = set()
|
|
638
|
+
if ns == "ta" and member in sigs.TA_FUNCTIONS:
|
|
639
|
+
for sig in sigs.TA_FUNCTIONS[member].signatures:
|
|
640
|
+
for i, p in enumerate(sig.params):
|
|
641
|
+
if p.pine_type == sigs.I:
|
|
642
|
+
length_params.update((i, p.name))
|
|
643
|
+
elif ns == "math" and member == "sum":
|
|
644
|
+
length_params.update((1, "length"))
|
|
645
|
+
if not length_params:
|
|
646
|
+
return
|
|
647
|
+
bound = [(i, a) for i, a in enumerate(call.args)] + list(call.kwargs.items())
|
|
648
|
+
for key, arg in bound:
|
|
649
|
+
if key in length_params and self._reads_reassigned(arg):
|
|
650
|
+
raise _Refusal(arg, (
|
|
651
|
+
f"a reassigned variable passed as {ns}.{member}()'s length: "
|
|
652
|
+
"v5 marks it const and runs the call at its first value, "
|
|
653
|
+
"which PineForge does not implement"))
|
|
654
|
+
|
|
655
|
+
def _reads_reassigned(self, expr) -> bool:
|
|
656
|
+
fn = self.fn
|
|
657
|
+
return any(
|
|
658
|
+
isinstance(node, Identifier)
|
|
659
|
+
and ((fn is not None and node.name in fn.reassigned)
|
|
660
|
+
or node.name in self.module.reassigned)
|
|
661
|
+
for node in _walk(expr))
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def lower_v5_modules(modules: list[tuple[LibraryModule, list, dict[str, str]]],
|
|
665
|
+
filename: str) -> None:
|
|
666
|
+
"""Lower the reachable definitions of every v5 module in ``modules``
|
|
667
|
+
(library, its inlined definitions in source order, its top-level renames)
|
|
668
|
+
with v5's rules, refusing by the library's name what PineForge does not
|
|
669
|
+
implement."""
|
|
670
|
+
for lib, definitions, names in modules:
|
|
671
|
+
if lib.pine_version != 5:
|
|
672
|
+
continue
|
|
673
|
+
module = _V5Module(lib, definitions, names)
|
|
674
|
+
for node in definitions:
|
|
675
|
+
try:
|
|
676
|
+
_Lowering(module, node).lower()
|
|
677
|
+
except _Refusal as exc:
|
|
678
|
+
loc = getattr(exc.node, "loc", None) or SourceLocation(
|
|
679
|
+
file=lib.path, line=1, col=1, end_col=1)
|
|
680
|
+
raise CompileError([Diagnostic(
|
|
681
|
+
level=Level.ERROR, phase=Phase.PARSER, location=loc,
|
|
682
|
+
message=f"library '{lib.path}' is //@version=5: {exc}",
|
|
683
|
+
)]) from None
|