@pineforge/codegen-pyodide 0.10.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -16
- package/glue.py +24 -16
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +125 -34
- package/pineforge_codegen/analyzer/__init__.py +2 -0
- package/pineforge_codegen/analyzer/base.py +754 -76
- package/pineforge_codegen/analyzer/call_handlers.py +260 -40
- package/pineforge_codegen/analyzer/contracts.py +37 -0
- package/pineforge_codegen/analyzer/diagnostics.py +30 -4
- package/pineforge_codegen/analyzer/tables.py +49 -8
- package/pineforge_codegen/analyzer/types.py +33 -1
- package/pineforge_codegen/ast_nodes.py +32 -1
- package/pineforge_codegen/block_locals.py +185 -0
- package/pineforge_codegen/builtin_keywords.py +42 -0
- package/pineforge_codegen/codegen/base.py +896 -156
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +946 -213
- package/pineforge_codegen/codegen/helpers.py +435 -14
- package/pineforge_codegen/codegen/host_members.py +162 -0
- package/pineforge_codegen/codegen/input.py +252 -85
- package/pineforge_codegen/codegen/security.py +4372 -377
- package/pineforge_codegen/codegen/session_market.py +71 -0
- package/pineforge_codegen/codegen/ta.py +1188 -100
- package/pineforge_codegen/codegen/tables.py +193 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +1882 -78
- package/pineforge_codegen/codegen/visit_call.py +920 -131
- package/pineforge_codegen/codegen/visit_expr.py +738 -57
- package/pineforge_codegen/codegen/visit_stmt.py +595 -49
- package/pineforge_codegen/external_requests.py +877 -0
- package/pineforge_codegen/lexer.py +104 -22
- package/pineforge_codegen/library_inline.py +1304 -0
- package/pineforge_codegen/library_modules.py +126 -0
- package/pineforge_codegen/library_v5.py +683 -0
- package/pineforge_codegen/limits.py +138 -0
- package/pineforge_codegen/method_binding.py +33 -0
- package/pineforge_codegen/parser.py +384 -68
- package/pineforge_codegen/pine_libraries.py +266 -0
- package/pineforge_codegen/pine_spelling.py +216 -0
- package/pineforge_codegen/pragmas.py +64 -10
- package/pineforge_codegen/security_contexts.py +1585 -0
- package/pineforge_codegen/session_reads.py +84 -0
- package/pineforge_codegen/signatures.py +48 -23
- package/pineforge_codegen/support_checker.py +1106 -85
- package/pineforge_codegen-1.0.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +23 -21
- package/transpile.worker.mjs +24 -16
- package/pineforge_codegen-0.10.4.tar.gz +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Bounded numeric folding for constructor expressions, without Python execution.
|
|
2
|
+
|
|
3
|
+
``ast`` only parses syntax here. No expression-supplied object, attribute or
|
|
4
|
+
callable is resolved: even ``math.sin`` is a fixed spelling in a local table.
|
|
5
|
+
Unsupported or non-finite expressions remain on the existing runtime/error path.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import math
|
|
12
|
+
import operator
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_BINARY = {
|
|
17
|
+
ast.Add: operator.add,
|
|
18
|
+
ast.Sub: operator.sub,
|
|
19
|
+
ast.Mult: operator.mul,
|
|
20
|
+
ast.Div: operator.truediv,
|
|
21
|
+
ast.Mod: operator.mod,
|
|
22
|
+
}
|
|
23
|
+
_UNARY = {ast.UAdd: operator.pos, ast.USub: operator.neg}
|
|
24
|
+
_FUNCTIONS = {
|
|
25
|
+
"abs": abs,
|
|
26
|
+
"round": round,
|
|
27
|
+
"math.abs": abs,
|
|
28
|
+
# Keep the existing folder's rounding behavior; this repair does not
|
|
29
|
+
# change numeric semantics to match a different rounding convention.
|
|
30
|
+
"math.round": round,
|
|
31
|
+
# sqrt/ceil/floor previously took a failed import rewrite and did not
|
|
32
|
+
# fold. Keep that domain unchanged: newly admitting sqrt under Python's
|
|
33
|
+
# round, for example, can disagree with the existing C++ runtime round.
|
|
34
|
+
"math.pow": math.pow,
|
|
35
|
+
"math.exp": math.exp,
|
|
36
|
+
"math.log": math.log,
|
|
37
|
+
"math.log10": math.log10,
|
|
38
|
+
"math.sin": math.sin,
|
|
39
|
+
"math.cos": math.cos,
|
|
40
|
+
"math.tan": math.tan,
|
|
41
|
+
"math.asin": math.asin,
|
|
42
|
+
"math.acos": math.acos,
|
|
43
|
+
"math.atan": math.atan,
|
|
44
|
+
}
|
|
45
|
+
_CONSTANTS = {"math.pi": math.pi, "math.e": math.e}
|
|
46
|
+
|
|
47
|
+
# Folding is optional. Bound parser work, tree traversal and integer growth;
|
|
48
|
+
# rejection retains the original expression for the caller's existing guard.
|
|
49
|
+
_MAX_SOURCE_LENGTH = 8192
|
|
50
|
+
_MAX_NODES = 512
|
|
51
|
+
_MAX_DEPTH = 64
|
|
52
|
+
_MAX_INTEGER_BITS = 1024
|
|
53
|
+
_MAX_ROUND_DIGITS = 1024
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _numeric(value: object) -> int | float | bool:
|
|
57
|
+
if type(value) is bool:
|
|
58
|
+
return value
|
|
59
|
+
if type(value) is int and value.bit_length() <= _MAX_INTEGER_BITS:
|
|
60
|
+
return value
|
|
61
|
+
if type(value) is float and math.isfinite(value):
|
|
62
|
+
return value
|
|
63
|
+
raise ValueError("not a bounded numeric constant")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _spelling(node: ast.AST) -> str:
|
|
67
|
+
if isinstance(node, ast.Name):
|
|
68
|
+
return node.id
|
|
69
|
+
if (isinstance(node, ast.Attribute)
|
|
70
|
+
and isinstance(node.value, ast.Name)
|
|
71
|
+
and node.value.id == "math"):
|
|
72
|
+
return f"math.{node.attr}"
|
|
73
|
+
raise ValueError("not an allowed numeric spelling")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def fold_numeric_expression(
|
|
77
|
+
expression: str, known_values: Mapping[str, object],
|
|
78
|
+
) -> int | float | bool | None:
|
|
79
|
+
"""Return a numeric constant, or None when the syntax cannot be folded.
|
|
80
|
+
|
|
81
|
+
Names resolve only to caller-provided primitive numbers. Calls resolve
|
|
82
|
+
only to the fixed functions above and accept positional numeric arguments.
|
|
83
|
+
Attribute chains, indexing, containers, comprehensions, lambda expressions,
|
|
84
|
+
arbitrary call targets and keyword/unpacked arguments are never evaluated.
|
|
85
|
+
"""
|
|
86
|
+
if len(expression) > _MAX_SOURCE_LENGTH:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def visit(node: ast.AST, depth: int = 0) -> int | float | bool:
|
|
90
|
+
if depth > _MAX_DEPTH:
|
|
91
|
+
raise ValueError("numeric expression is too deep")
|
|
92
|
+
if isinstance(node, ast.Constant):
|
|
93
|
+
return _numeric(node.value)
|
|
94
|
+
if isinstance(node, ast.Name):
|
|
95
|
+
return _numeric(known_values[node.id])
|
|
96
|
+
if isinstance(node, ast.Attribute):
|
|
97
|
+
return _CONSTANTS[_spelling(node)]
|
|
98
|
+
if isinstance(node, ast.UnaryOp) and type(node.op) in _UNARY:
|
|
99
|
+
return _numeric(_UNARY[type(node.op)](visit(node.operand, depth + 1)))
|
|
100
|
+
if isinstance(node, ast.BinOp) and type(node.op) in _BINARY:
|
|
101
|
+
return _numeric(_BINARY[type(node.op)](
|
|
102
|
+
visit(node.left, depth + 1), visit(node.right, depth + 1),
|
|
103
|
+
))
|
|
104
|
+
if isinstance(node, ast.Call) and not node.keywords:
|
|
105
|
+
spelling = _spelling(node.func)
|
|
106
|
+
if spelling.split(".", 1)[0] in known_values:
|
|
107
|
+
raise ValueError("numeric function name is shadowed")
|
|
108
|
+
function = _FUNCTIONS[spelling]
|
|
109
|
+
args = [visit(arg, depth + 1) for arg in node.args]
|
|
110
|
+
if spelling in ("round", "math.round"):
|
|
111
|
+
# CPython's integer round builds 10**(-ndigits). A small
|
|
112
|
+
# integer operand can still request enormous work, so bound
|
|
113
|
+
# this argument BEFORE dispatch, not only the returned value.
|
|
114
|
+
if not 1 <= len(args) <= 2:
|
|
115
|
+
raise ValueError("unsupported round argument count")
|
|
116
|
+
if len(args) == 2 and (
|
|
117
|
+
type(args[1]) not in (int, bool)
|
|
118
|
+
or abs(args[1]) > _MAX_ROUND_DIGITS
|
|
119
|
+
):
|
|
120
|
+
raise ValueError("round precision is not bounded")
|
|
121
|
+
return _numeric(function(*args))
|
|
122
|
+
raise ValueError("not numeric expression syntax")
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
tree = ast.parse(expression, mode="eval")
|
|
126
|
+
if sum(1 for _ in ast.walk(tree)) > _MAX_NODES:
|
|
127
|
+
return None
|
|
128
|
+
return visit(tree.body)
|
|
129
|
+
except (SyntaxError, ValueError, TypeError, KeyError, ArithmeticError,
|
|
130
|
+
RecursionError):
|
|
131
|
+
return None
|
|
@@ -19,6 +19,8 @@ reads only attributes established by ``CodeGen.__init__`` (``_uses_drawing``,
|
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
|
+
import re
|
|
23
|
+
|
|
22
24
|
from ..ast_nodes import FuncCall, Identifier, MemberAccess
|
|
23
25
|
from ..symbols import method_receiver_type_name
|
|
24
26
|
from .tables import (
|
|
@@ -30,6 +32,74 @@ from .tables import (
|
|
|
30
32
|
|
|
31
33
|
_MAP_VOID_METHODS = frozenset({"clear", "put_all"})
|
|
32
34
|
|
|
35
|
+
# Builtin calls Pine types as void, each lowering to a void C++ statement. A
|
|
36
|
+
# user function or selection arm ending in one returns no value, so it must be
|
|
37
|
+
# emitted as a statement, never as ``return strategy_exit(...)``.
|
|
38
|
+
_VOID_BUILTIN_CALLS = frozenset({
|
|
39
|
+
("strategy", "entry"), ("strategy", "order"), ("strategy", "exit"),
|
|
40
|
+
("strategy", "close"), ("strategy", "close_all"),
|
|
41
|
+
("strategy", "cancel"), ("strategy", "cancel_all"),
|
|
42
|
+
("log", "info"), ("log", "warning"), ("log", "error"),
|
|
43
|
+
("runtime", "error"),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
# TradingView's drawing lifetime, emitted once per drawing TU after
|
|
47
|
+
# ``using namespace pineforge;``. The engine's arena keeps a deleted record
|
|
48
|
+
# readable and evicts at exactly the cap; TradingView does neither (lane
|
|
49
|
+
# CG-W9-MISC tapes, tests/fixtures/drawing_lifetime). The generated arenas are
|
|
50
|
+
# therefore unbounded (``_PF_DRAWING_UNBOUNDED``), every getter and setter goes
|
|
51
|
+
# through the liveness check below, and ``_pf_collect_<kind>s_`` (a member of
|
|
52
|
+
# the strategy, which knows its variables) runs the collection.
|
|
53
|
+
DRAWING_LIFETIME_CPP = """\
|
|
54
|
+
// TradingView's drawing lifetime. A deleted or collected drawing reads like a
|
|
55
|
+
// na handle: every getter returns na, every setter does nothing and na() is
|
|
56
|
+
// true. A new line, box or label that makes its kind's live count reach
|
|
57
|
+
// max_<kind>_count + 6 deletes the oldest drawings of that kind that no var,
|
|
58
|
+
// varip or history-read variable holds, until max_<kind>_count remain; one
|
|
59
|
+
// held only by an array, a map, an object field, a local or a plain non-var
|
|
60
|
+
// variable is collectable. Linefills are never collected.
|
|
61
|
+
constexpr int _PF_DRAWING_UNBOUNDED = 2147483647;
|
|
62
|
+
template <class T> struct _PFDrawingArg { using type = T; };
|
|
63
|
+
template <class R, class Rec, class H, class... A>
|
|
64
|
+
inline R _pf_drawing_get(R (*get)(DrawingArena<Rec>&, H, A...),
|
|
65
|
+
typename _PFDrawingArg<DrawingArena<Rec>&>::type arena,
|
|
66
|
+
typename _PFDrawingArg<H>::type h,
|
|
67
|
+
typename _PFDrawingArg<A>::type... args) {
|
|
68
|
+
return arena.alive(h.id) ? get(arena, h, args...) : na<R>();
|
|
69
|
+
}
|
|
70
|
+
template <class Rec, class H, class... A>
|
|
71
|
+
inline void _pf_drawing_set(void (*set)(DrawingArena<Rec>&, H, A...),
|
|
72
|
+
typename _PFDrawingArg<DrawingArena<Rec>&>::type arena,
|
|
73
|
+
typename _PFDrawingArg<H>::type h,
|
|
74
|
+
typename _PFDrawingArg<A>::type... args) {
|
|
75
|
+
if (arena.alive(h.id)) set(arena, h, args...);
|
|
76
|
+
}
|
|
77
|
+
template <class Rec, class H>
|
|
78
|
+
inline bool _pf_drawing_na(const DrawingArena<Rec>& arena, H h) {
|
|
79
|
+
return !arena.alive(h.id);
|
|
80
|
+
}
|
|
81
|
+
template <class Rec>
|
|
82
|
+
inline void _pf_collect_drawings(DrawingArena<Rec>& arena, int keep,
|
|
83
|
+
const int32_t* pins, std::size_t pin_count) {
|
|
84
|
+
const std::size_t excess = arena.order().size() - static_cast<std::size_t>(keep);
|
|
85
|
+
std::vector<int32_t> doomed;
|
|
86
|
+
for (int32_t id : arena.order()) {
|
|
87
|
+
if (doomed.size() == excess) break;
|
|
88
|
+
if (std::find(pins, pins + pin_count, id) == pins + pin_count) doomed.push_back(id);
|
|
89
|
+
}
|
|
90
|
+
for (int32_t id : doomed) arena.erase(id);
|
|
91
|
+
}
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
# Drawing kinds TradingView collects, with their member collector.
|
|
95
|
+
DRAWING_COLLECTED = {
|
|
96
|
+
"line": "_pf_collect_lines_",
|
|
97
|
+
"box": "_pf_collect_boxes_",
|
|
98
|
+
"label": "_pf_collect_labels_",
|
|
99
|
+
}
|
|
100
|
+
# TradingView keeps up to max_<kind>_count + 5 live drawings of a kind.
|
|
101
|
+
DRAWING_COLLECTION_SLACK = 6
|
|
102
|
+
|
|
33
103
|
# ---------------------------------------------------------------------------
|
|
34
104
|
# Canonical Pine v6 constructor param-name lists (positional order).
|
|
35
105
|
# Only the GEOMETRY names are consumed downstream; every other (visual) name is
|
|
@@ -197,7 +267,16 @@ class DrawingVisitor:
|
|
|
197
267
|
return True
|
|
198
268
|
return False
|
|
199
269
|
|
|
270
|
+
@staticmethod
|
|
271
|
+
def _collected(dtype: str, handle: str) -> str:
|
|
272
|
+
"""A new drawing of a collected kind passes through its collector."""
|
|
273
|
+
collector = DRAWING_COLLECTED.get(dtype)
|
|
274
|
+
return f"{collector}({handle})" if collector else handle
|
|
275
|
+
|
|
200
276
|
def _emit_drawing_ctor(self, dtype: str, node: FuncCall) -> str:
|
|
277
|
+
return self._collected(dtype, self._emit_drawing_alloc(dtype, node))
|
|
278
|
+
|
|
279
|
+
def _emit_drawing_alloc(self, dtype: str, node: FuncCall) -> str:
|
|
201
280
|
arena = DRAWING_ARENA[dtype]
|
|
202
281
|
if dtype == "linefill":
|
|
203
282
|
vals = self._merge_drawing_args(node, ["line1", "line2", "color"])
|
|
@@ -219,8 +298,10 @@ class DrawingVisitor:
|
|
|
219
298
|
y1 = self._visit_expr(vals.get("y1"))
|
|
220
299
|
x2 = self._visit_expr(vals.get("x2"))
|
|
221
300
|
y2 = self._visit_expr(vals.get("y2"))
|
|
222
|
-
|
|
223
|
-
|
|
301
|
+
x1 = self._coerce_int_slot_with_cast(x1, vals.get("x1"), "int64_t")
|
|
302
|
+
x2 = self._coerce_int_slot_with_cast(x2, vals.get("x2"), "int64_t")
|
|
303
|
+
return (f"pf_line_new({arena}, {x1}, (double)({y1}), "
|
|
304
|
+
f"{x2}, (double)({y2}), {xloc}, {el}, {er})")
|
|
224
305
|
|
|
225
306
|
if dtype == "box":
|
|
226
307
|
vals = self._merge_drawing_args(node, _BOX_CTOR_PTS if use_pts else _BOX_CTOR)
|
|
@@ -233,8 +314,10 @@ class DrawingVisitor:
|
|
|
233
314
|
top = self._visit_expr(vals.get("top"))
|
|
234
315
|
right = self._visit_expr(vals.get("right"))
|
|
235
316
|
bottom = self._visit_expr(vals.get("bottom"))
|
|
236
|
-
|
|
237
|
-
|
|
317
|
+
left = self._coerce_int_slot_with_cast(left, vals.get("left"), "int64_t")
|
|
318
|
+
right = self._coerce_int_slot_with_cast(right, vals.get("right"), "int64_t")
|
|
319
|
+
return (f"pf_box_new({arena}, {left}, (double)({top}), "
|
|
320
|
+
f"{right}, (double)({bottom}), {xloc})")
|
|
238
321
|
|
|
239
322
|
if dtype == "label":
|
|
240
323
|
vals = self._merge_drawing_args(node, _LABEL_CTOR_PTS if use_pts else _LABEL_CTOR)
|
|
@@ -247,7 +330,8 @@ class DrawingVisitor:
|
|
|
247
330
|
x = self._visit_expr(vals.get("x"))
|
|
248
331
|
y = self._visit_expr(vals.get("y"))
|
|
249
332
|
xloc = self._lower_xloc(vals.get("xloc"))
|
|
250
|
-
|
|
333
|
+
x = self._coerce_int_slot_with_cast(x, vals.get("x"), "int64_t")
|
|
334
|
+
return (f"pf_label_new({arena}, {x}, (double)({y}), "
|
|
251
335
|
f"{text}, {xloc}, {yloc})")
|
|
252
336
|
|
|
253
337
|
return "0" # unreachable
|
|
@@ -269,20 +353,24 @@ class DrawingVisitor:
|
|
|
269
353
|
vals = self._merge_drawing_args(node, ["index", "price"])
|
|
270
354
|
idx = self._visit_expr(vals.get("index"))
|
|
271
355
|
price = self._visit_expr(vals.get("price"))
|
|
272
|
-
|
|
356
|
+
idx = self._coerce_int_slot_with_cast(idx, vals.get("index"), "int64_t")
|
|
357
|
+
return (f"ChartPoint{{ .index={idx}, .time=na<int64_t>(), "
|
|
273
358
|
f".price=({price}) }}")
|
|
274
359
|
if func_name == "from_time":
|
|
275
360
|
vals = self._merge_drawing_args(node, ["time", "price"])
|
|
276
361
|
tm = self._visit_expr(vals.get("time"))
|
|
277
362
|
price = self._visit_expr(vals.get("price"))
|
|
278
|
-
|
|
363
|
+
tm = self._coerce_int_slot_with_cast(tm, vals.get("time"), "int64_t")
|
|
364
|
+
return (f"ChartPoint{{ .index=na<int64_t>(), .time={tm}, "
|
|
279
365
|
f".price=({price}) }}")
|
|
280
366
|
if func_name == "new":
|
|
281
367
|
vals = self._merge_drawing_args(node, ["time", "index", "price"])
|
|
282
368
|
tm = self._visit_expr(vals.get("time"))
|
|
283
369
|
idx = self._visit_expr(vals.get("index"))
|
|
284
370
|
price = self._visit_expr(vals.get("price"))
|
|
285
|
-
|
|
371
|
+
idx = self._coerce_int_slot_with_cast(idx, vals.get("index"), "int64_t")
|
|
372
|
+
tm = self._coerce_int_slot_with_cast(tm, vals.get("time"), "int64_t")
|
|
373
|
+
return (f"ChartPoint{{ .index={idx}, .time={tm}, "
|
|
286
374
|
f".price=({price}) }}")
|
|
287
375
|
return "ChartPoint{}"
|
|
288
376
|
|
|
@@ -319,106 +407,88 @@ class DrawingVisitor:
|
|
|
319
407
|
return self._emit_linefill_method(method, arena, recv, av)
|
|
320
408
|
return "0"
|
|
321
409
|
|
|
410
|
+
# Getters and setters reach the arena through ``_pf_drawing_get`` /
|
|
411
|
+
# ``_pf_drawing_set`` (DRAWING_LIFETIME_CPP): a deleted, collected or na
|
|
412
|
+
# handle reads na and ignores the write. Every argument is still evaluated.
|
|
413
|
+
@staticmethod
|
|
414
|
+
def _live_get(fn: str, a: str, r: str, *args: str) -> str:
|
|
415
|
+
extra = "".join(", " + x for x in args)
|
|
416
|
+
return f"_pf_drawing_get({fn}, {a}, {r}{extra})"
|
|
417
|
+
|
|
418
|
+
@staticmethod
|
|
419
|
+
def _live_set(fn: str, a: str, r: str, *args: str) -> str:
|
|
420
|
+
extra = "".join(", " + x for x in args)
|
|
421
|
+
return f"_pf_drawing_set({fn}, {a}, {r}{extra})"
|
|
422
|
+
|
|
322
423
|
def _emit_line_method(self, m, a, r, av, raw) -> str:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return f"pf_line_get_x2({a}, {r})"
|
|
327
|
-
if m == "get_y1":
|
|
328
|
-
return f"pf_line_get_y1({a}, {r})"
|
|
329
|
-
if m == "get_y2":
|
|
330
|
-
return f"pf_line_get_y2({a}, {r})"
|
|
424
|
+
get, put = self._live_get, self._live_set
|
|
425
|
+
if m in ("get_x1", "get_x2", "get_y1", "get_y2"):
|
|
426
|
+
return get(f"pf_line_{m}", a, r)
|
|
331
427
|
if m == "get_price":
|
|
332
|
-
return
|
|
333
|
-
if m
|
|
334
|
-
return f"
|
|
335
|
-
if m
|
|
336
|
-
return f"
|
|
337
|
-
if m
|
|
338
|
-
return f"
|
|
339
|
-
if m
|
|
340
|
-
return f"
|
|
341
|
-
if m == "set_xy1":
|
|
342
|
-
return f"pf_line_set_xy1({a}, {r}, (int64_t)({av[0]}), (double)({av[1]}))"
|
|
343
|
-
if m == "set_xy2":
|
|
344
|
-
return f"pf_line_set_xy2({a}, {r}, (int64_t)({av[0]}), (double)({av[1]}))"
|
|
345
|
-
if m == "set_first_point":
|
|
346
|
-
return f"pf_line_set_first_point({a}, {r}, {av[0]})"
|
|
347
|
-
if m == "set_second_point":
|
|
348
|
-
return f"pf_line_set_second_point({a}, {r}, {av[0]})"
|
|
428
|
+
return get("pf_line_get_price", a, r, self._coerce_int_slot_with_cast(av[0], raw[0], 'int64_t'))
|
|
429
|
+
if m in ("set_x1", "set_x2"):
|
|
430
|
+
return put(f"pf_line_{m}", a, r, self._coerce_int_slot_with_cast(av[0], raw[0], 'int64_t'))
|
|
431
|
+
if m in ("set_y1", "set_y2"):
|
|
432
|
+
return put(f"pf_line_{m}", a, r, f"(double)({av[0]})")
|
|
433
|
+
if m in ("set_xy1", "set_xy2"):
|
|
434
|
+
return put(f"pf_line_{m}", a, r, self._coerce_int_slot_with_cast(av[0], raw[0], 'int64_t'), f"(double)({av[1]})")
|
|
435
|
+
if m in ("set_first_point", "set_second_point"):
|
|
436
|
+
return put(f"pf_line_{m}", a, r, av[0])
|
|
349
437
|
if m == "set_xloc":
|
|
350
|
-
return
|
|
438
|
+
return put("pf_line_set_xloc", a, r, self._coerce_int_slot_with_cast(av[0], raw[0], 'int64_t'), self._coerce_int_slot_with_cast(av[1], raw[1], 'int64_t'), self._lower_xloc(raw[2]))
|
|
351
439
|
if m == "copy":
|
|
352
|
-
return f"pf_line_copy({a}, {r})"
|
|
440
|
+
return self._collected("line", f"pf_line_copy({a}, {r})")
|
|
353
441
|
if m == "delete":
|
|
354
442
|
return f"pf_line_delete({a}, {r})"
|
|
355
443
|
return "0"
|
|
356
444
|
|
|
357
445
|
def _emit_box_method(self, m, a, r, av, raw) -> str:
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return f"pf_box_set_right({a}, {r}, (int64_t)({av[0]}))"
|
|
370
|
-
if m == "set_top":
|
|
371
|
-
return f"pf_box_set_top({a}, {r}, (double)({av[0]}))"
|
|
372
|
-
if m == "set_bottom":
|
|
373
|
-
return f"pf_box_set_bottom({a}, {r}, (double)({av[0]}))"
|
|
374
|
-
if m == "set_lefttop":
|
|
375
|
-
return f"pf_box_set_lefttop({a}, {r}, (int64_t)({av[0]}), (double)({av[1]}))"
|
|
376
|
-
if m == "set_rightbottom":
|
|
377
|
-
return f"pf_box_set_rightbottom({a}, {r}, (int64_t)({av[0]}), (double)({av[1]}))"
|
|
378
|
-
if m == "set_top_left_point":
|
|
379
|
-
return f"pf_box_set_top_left_point({a}, {r}, {av[0]})"
|
|
380
|
-
if m == "set_bottom_right_point":
|
|
381
|
-
return f"pf_box_set_bottom_right_point({a}, {r}, {av[0]})"
|
|
446
|
+
get, put = self._live_get, self._live_set
|
|
447
|
+
if m in ("get_left", "get_right", "get_top", "get_bottom"):
|
|
448
|
+
return get(f"pf_box_{m}", a, r)
|
|
449
|
+
if m in ("set_left", "set_right"):
|
|
450
|
+
return put(f"pf_box_{m}", a, r, self._coerce_int_slot_with_cast(av[0], raw[0], 'int64_t'))
|
|
451
|
+
if m in ("set_top", "set_bottom"):
|
|
452
|
+
return put(f"pf_box_{m}", a, r, f"(double)({av[0]})")
|
|
453
|
+
if m in ("set_lefttop", "set_rightbottom"):
|
|
454
|
+
return put(f"pf_box_{m}", a, r, self._coerce_int_slot_with_cast(av[0], raw[0], 'int64_t'), f"(double)({av[1]})")
|
|
455
|
+
if m in ("set_top_left_point", "set_bottom_right_point"):
|
|
456
|
+
return put(f"pf_box_{m}", a, r, av[0])
|
|
382
457
|
if m == "set_xloc":
|
|
383
|
-
return
|
|
458
|
+
return put("pf_box_set_xloc", a, r, self._coerce_int_slot_with_cast(av[0], raw[0], 'int64_t'), self._coerce_int_slot_with_cast(av[1], raw[1], 'int64_t'), self._lower_xloc(raw[2]))
|
|
384
459
|
if m == "copy":
|
|
385
|
-
return f"pf_box_copy({a}, {r})"
|
|
460
|
+
return self._collected("box", f"pf_box_copy({a}, {r})")
|
|
386
461
|
if m == "delete":
|
|
387
462
|
return f"pf_box_delete({a}, {r})"
|
|
388
463
|
return "0"
|
|
389
464
|
|
|
390
465
|
def _emit_label_method(self, m, a, r, av, raw) -> str:
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return f"pf_label_get_y({a}, {r})"
|
|
395
|
-
if m == "get_text":
|
|
396
|
-
return f"pf_label_get_text({a}, {r})"
|
|
466
|
+
get, put = self._live_get, self._live_set
|
|
467
|
+
if m in ("get_x", "get_y", "get_text"):
|
|
468
|
+
return get(f"pf_label_{m}", a, r)
|
|
397
469
|
if m == "set_x":
|
|
398
|
-
return
|
|
470
|
+
return put("pf_label_set_x", a, r, self._coerce_int_slot_with_cast(av[0], raw[0], 'int64_t'))
|
|
399
471
|
if m == "set_y":
|
|
400
|
-
return
|
|
472
|
+
return put("pf_label_set_y", a, r, f"(double)({av[0]})")
|
|
401
473
|
if m == "set_xy":
|
|
402
|
-
return
|
|
474
|
+
return put("pf_label_set_xy", a, r, self._coerce_int_slot_with_cast(av[0], raw[0], 'int64_t'), f"(double)({av[1]})")
|
|
403
475
|
if m == "set_point":
|
|
404
|
-
return
|
|
476
|
+
return put("pf_label_set_point", a, r, av[0])
|
|
405
477
|
if m == "set_xloc":
|
|
406
|
-
return
|
|
478
|
+
return put("pf_label_set_xloc", a, r, self._coerce_int_slot_with_cast(av[0], raw[0], 'int64_t'), self._lower_xloc(raw[1]))
|
|
407
479
|
if m == "set_yloc":
|
|
408
|
-
return
|
|
480
|
+
return put("pf_label_set_yloc", a, r, self._lower_yloc(raw[0]))
|
|
409
481
|
if m == "set_text":
|
|
410
|
-
return
|
|
482
|
+
return put("pf_label_set_text", a, r, av[0])
|
|
411
483
|
if m == "copy":
|
|
412
|
-
return f"pf_label_copy({a}, {r})"
|
|
484
|
+
return self._collected("label", f"pf_label_copy({a}, {r})")
|
|
413
485
|
if m == "delete":
|
|
414
486
|
return f"pf_label_delete({a}, {r})"
|
|
415
487
|
return "0"
|
|
416
488
|
|
|
417
489
|
def _emit_linefill_method(self, m, a, r, av) -> str:
|
|
418
|
-
if m
|
|
419
|
-
return f"
|
|
420
|
-
if m == "get_line2":
|
|
421
|
-
return f"pf_linefill_get_line2({a}, {r})"
|
|
490
|
+
if m in ("get_line1", "get_line2"):
|
|
491
|
+
return self._live_get(f"pf_linefill_{m}", a, r)
|
|
422
492
|
if m == "delete":
|
|
423
493
|
return f"pf_linefill_delete({a}, {r})"
|
|
424
494
|
return "0"
|
|
@@ -516,6 +586,16 @@ class DrawingVisitor:
|
|
|
516
586
|
|
|
517
587
|
if self._drawing_call_is_void(node):
|
|
518
588
|
return True
|
|
589
|
+
receiver = node.callee.object
|
|
590
|
+
if (isinstance(receiver, Identifier)
|
|
591
|
+
and (receiver.name, method) in _VOID_BUILTIN_CALLS):
|
|
592
|
+
return True
|
|
593
|
+
if (isinstance(receiver, MemberAccess)
|
|
594
|
+
and isinstance(receiver.object, Identifier)
|
|
595
|
+
and receiver.object.name == "strategy"
|
|
596
|
+
and receiver.member == "risk"):
|
|
597
|
+
# strategy.risk.* setters are statements (see _visit_stmt).
|
|
598
|
+
return True
|
|
519
599
|
_fn, ns = self._resolve_callee(node.callee)
|
|
520
600
|
|
|
521
601
|
if method in _MATRIX_VOID_METHODS:
|
|
@@ -571,6 +651,68 @@ class DrawingVisitor:
|
|
|
571
651
|
recv_spec = param_spec or self._type_spec_from_expr(node.callee.object)
|
|
572
652
|
return recv_spec is not None and recv_spec.kind == "map"
|
|
573
653
|
|
|
654
|
+
# ------------------------------------------------------------------
|
|
655
|
+
# Drawing lifetime: na(), the collection and its pins
|
|
656
|
+
# ------------------------------------------------------------------
|
|
657
|
+
def _drawing_na_expr(self, arg) -> str | None:
|
|
658
|
+
"""``na(<line|box|label|linefill>)`` asks the arena, which knows a
|
|
659
|
+
deleted or collected drawing; ``is_na`` sees only a na handle."""
|
|
660
|
+
if not getattr(self, "_uses_drawing", False):
|
|
661
|
+
return None
|
|
662
|
+
spec = self._type_spec_from_expr(arg)
|
|
663
|
+
if spec is None or spec.kind != "udt" or spec.name not in DRAWING_ARENA:
|
|
664
|
+
return None
|
|
665
|
+
return f"_pf_drawing_na({DRAWING_ARENA[spec.name]}, {self._visit_expr(arg)})"
|
|
666
|
+
|
|
667
|
+
_DRAWING_PIN_DECL = re.compile(
|
|
668
|
+
r"^ (?:Series<(Line|Box|Label)>|(Line|Box|Label)) (\w+)\b")
|
|
669
|
+
|
|
670
|
+
@classmethod
|
|
671
|
+
def _drawing_pin_members(cls, declarations: list[str]) -> dict[str, list[str]]:
|
|
672
|
+
"""The current value of every drawing-typed pinning variable, per kind.
|
|
673
|
+
|
|
674
|
+
``declarations`` are the generated member lines of the ``var`` /
|
|
675
|
+
``varip`` variables (global, block and callable, with their
|
|
676
|
+
per-call-site and fresh clones) and of the non-var variables whose
|
|
677
|
+
history is read. TradingView keeps a drawing one of them holds. A
|
|
678
|
+
drawing held only by an array, an object field, a local or a plain
|
|
679
|
+
non-var variable is collectable, as is one a ``var`` held before it
|
|
680
|
+
was reassigned (tests/fixtures/drawing_lifetime ``w9dg-pin-*``).
|
|
681
|
+
"""
|
|
682
|
+
kinds = {"Line": "line", "Box": "box", "Label": "label"}
|
|
683
|
+
pins: dict[str, list[str]] = {kind: [] for kind in kinds.values()}
|
|
684
|
+
for line in declarations:
|
|
685
|
+
match = cls._DRAWING_PIN_DECL.match(line)
|
|
686
|
+
if match is None:
|
|
687
|
+
continue
|
|
688
|
+
series, plain, name = match.groups()
|
|
689
|
+
current = f"this->{name}[0]" if series else f"this->{name}"
|
|
690
|
+
pins[kinds[series or plain]].append(f"{current}.id")
|
|
691
|
+
return pins
|
|
692
|
+
|
|
693
|
+
def _emit_drawing_collectors(self, lines: list[str]) -> None:
|
|
694
|
+
"""One collector per collected kind: a new drawing passes through it,
|
|
695
|
+
and the one that makes the kind's live count reach its
|
|
696
|
+
max_<kind>_count + 6 deletes the oldest drawings no pinning variable
|
|
697
|
+
(``_drawing_pin_members``) holds."""
|
|
698
|
+
pins = getattr(self, "_drawing_pins", None) or {}
|
|
699
|
+
caps = self._drawing_caps or {}
|
|
700
|
+
for dtype, collector in DRAWING_COLLECTED.items():
|
|
701
|
+
handle = DRAWING_TYPE_TO_CPP[dtype]
|
|
702
|
+
arena = DRAWING_ARENA[dtype]
|
|
703
|
+
keep = caps.get(dtype, 50)
|
|
704
|
+
held = ", ".join(["_pf_new.id", *pins.get(dtype, ())])
|
|
705
|
+
lines.extend([
|
|
706
|
+
f" {handle} {collector}({handle} _pf_new) {{",
|
|
707
|
+
f" if ((int)this->{arena}.order().size() >= {keep + DRAWING_COLLECTION_SLACK}) {{",
|
|
708
|
+
f" const int32_t _pf_held[] = {{{held}}};",
|
|
709
|
+
f" _pf_collect_drawings(this->{arena}, {keep}, _pf_held,",
|
|
710
|
+
" sizeof(_pf_held) / sizeof(_pf_held[0]));",
|
|
711
|
+
" }",
|
|
712
|
+
" return _pf_new;",
|
|
713
|
+
" }",
|
|
714
|
+
])
|
|
715
|
+
|
|
574
716
|
# ------------------------------------------------------------------
|
|
575
717
|
# _uses_drawing detection + arena caps
|
|
576
718
|
# ------------------------------------------------------------------
|