@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
|
@@ -87,8 +87,11 @@ classes from ``..ast_nodes``.
|
|
|
87
87
|
|
|
88
88
|
from __future__ import annotations
|
|
89
89
|
|
|
90
|
+
from ..errors import Phase
|
|
91
|
+
from ..external_requests import UNPINNED_ANNOTATION
|
|
90
92
|
from ..ast_nodes import (
|
|
91
93
|
ASTNode,
|
|
94
|
+
Assignment,
|
|
92
95
|
BinOp,
|
|
93
96
|
BoolLiteral,
|
|
94
97
|
ColorLiteral,
|
|
@@ -103,8 +106,11 @@ from ..ast_nodes import (
|
|
|
103
106
|
TupleLiteral,
|
|
104
107
|
UnaryOp,
|
|
105
108
|
)
|
|
109
|
+
from .helpers import pine_index_int_cast, unary_sign_cpp
|
|
110
|
+
from .types import COLLECTION_MUTATING_METHODS
|
|
106
111
|
from .tables import (
|
|
107
112
|
ADJUSTMENT_MAP,
|
|
113
|
+
ALERT_FREQ_VALUES,
|
|
108
114
|
BAR_BUILTINS,
|
|
109
115
|
BAR_FIELDS,
|
|
110
116
|
BAR_SERIES_PUSH,
|
|
@@ -180,6 +186,50 @@ _RELATIONAL_OPS: frozenset[str] = frozenset({"==", "!=", "<", ">", "<=", ">="})
|
|
|
180
186
|
# na-bool is engine-indistinguishable from ``false`` (na<bool>() == false).
|
|
181
187
|
_NA_SCALAR_CPP: frozenset[str] = frozenset({"int", "int64_t", "double"})
|
|
182
188
|
|
|
189
|
+
# What a binary operand no effect of the other operand can change is built
|
|
190
|
+
# from (``_binop_operand_is_order_free``): literals and variables, these
|
|
191
|
+
# calls, and operators over them.
|
|
192
|
+
_ORDER_FREE_LITERALS = (
|
|
193
|
+
NumberLiteral, StringLiteral, BoolLiteral, NaLiteral, ColorLiteral,
|
|
194
|
+
)
|
|
195
|
+
_ORDER_FREE_CALL_NAMESPACES = frozenset({"str", "math", "color", "ta"})
|
|
196
|
+
_ORDER_FREE_CALLS = frozenset({"nz", "na", "int", "float", "bool", "string"})
|
|
197
|
+
# Builtin calls with an effect whose order is observable
|
|
198
|
+
# (``_expr_has_ordered_effect``): an order, a log line, a script stop.
|
|
199
|
+
_ORDERED_EFFECT_CALLS: frozenset[tuple[str, str]] = frozenset({
|
|
200
|
+
("strategy", "entry"), ("strategy", "order"), ("strategy", "exit"),
|
|
201
|
+
("strategy", "close"), ("strategy", "close_all"),
|
|
202
|
+
("strategy", "cancel"), ("strategy", "cancel_all"),
|
|
203
|
+
("log", "info"), ("log", "warning"), ("log", "error"),
|
|
204
|
+
("runtime", "error"),
|
|
205
|
+
})
|
|
206
|
+
# Every call of these namespaces but a ``get_*`` getter creates, copies,
|
|
207
|
+
# changes or deletes a drawing; the method forms are ``set_*``,
|
|
208
|
+
# ``cell_set_*`` and these.
|
|
209
|
+
_DRAWING_EFFECT_NAMESPACES = frozenset({
|
|
210
|
+
"label", "line", "box", "table", "polyline", "linefill",
|
|
211
|
+
})
|
|
212
|
+
_DRAWING_EFFECT_METHODS = frozenset({"delete", "cell", "merge_cells"})
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _ast_children(node):
|
|
216
|
+
"""The AST nodes directly under ``node``: its fields, lists, tuples (a
|
|
217
|
+
``switch`` case) and dicts (keyword arguments), not its annotations."""
|
|
218
|
+
for name, value in vars(node).items():
|
|
219
|
+
if name not in ("loc", "annotations"):
|
|
220
|
+
yield from _ast_nodes_in(value)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _ast_nodes_in(value):
|
|
224
|
+
if isinstance(value, ASTNode):
|
|
225
|
+
yield value
|
|
226
|
+
elif isinstance(value, (list, tuple)):
|
|
227
|
+
for item in value:
|
|
228
|
+
yield from _ast_nodes_in(item)
|
|
229
|
+
elif isinstance(value, dict):
|
|
230
|
+
for item in value.values():
|
|
231
|
+
yield from _ast_nodes_in(item)
|
|
232
|
+
|
|
183
233
|
|
|
184
234
|
class ExprVisitor:
|
|
185
235
|
"""Expression-level visitor methods shared across the codegen.
|
|
@@ -194,6 +244,16 @@ class ExprVisitor:
|
|
|
194
244
|
def _visit_expr(self, node: ASTNode | None) -> str:
|
|
195
245
|
if node is None:
|
|
196
246
|
return "/* null */"
|
|
247
|
+
if self._budget is not None:
|
|
248
|
+
self._budget_visit_count += 1
|
|
249
|
+
if self._budget_visit_count % 128 == 0:
|
|
250
|
+
self._budget.check(node.loc, Phase.CODEGEN)
|
|
251
|
+
if node.annotations and UNPINNED_ANNOTATION in node.annotations:
|
|
252
|
+
return self._unpinned_read(node)
|
|
253
|
+
if self._security_fallback_frame is not None:
|
|
254
|
+
delegated = self._security_fallback_delegate(node)
|
|
255
|
+
if delegated is not None:
|
|
256
|
+
return delegated
|
|
197
257
|
if isinstance(node, NumberLiteral):
|
|
198
258
|
return str(node.value)
|
|
199
259
|
if isinstance(node, StringLiteral):
|
|
@@ -213,10 +273,7 @@ class ExprVisitor:
|
|
|
213
273
|
if isinstance(node, UnaryOp):
|
|
214
274
|
return self._visit_unaryop(node)
|
|
215
275
|
if isinstance(node, Ternary):
|
|
216
|
-
|
|
217
|
-
t = self._visit_expr(node.true_val)
|
|
218
|
-
f = self._visit_expr(node.false_val)
|
|
219
|
-
return f"(({c}) ? ({t}) : ({f}))"
|
|
276
|
+
return self._visit_ternary(node)
|
|
220
277
|
if isinstance(node, FuncCall):
|
|
221
278
|
return self._visit_func_call(node)
|
|
222
279
|
if isinstance(node, Subscript):
|
|
@@ -240,9 +297,55 @@ class ExprVisitor:
|
|
|
240
297
|
return f"std::make_tuple({elems})"
|
|
241
298
|
return "/* unknown */"
|
|
242
299
|
|
|
300
|
+
# A ``?:`` chain continued in its false arms (``a ? x : b ? y : z``) of at
|
|
301
|
+
# least this many links is emitted without the per-link brackets: they add
|
|
302
|
+
# three bracket levels per link and clang stops at 256
|
|
303
|
+
# (``-fbracket-depth``). C++'s conditional operator is right-associative,
|
|
304
|
+
# so ``a ? x : b ? y : z`` reads ``a ? x : (b ? y : z)``. Shorter chains
|
|
305
|
+
# keep their historical spelling byte for byte.
|
|
306
|
+
_FLAT_TERNARY_CHAIN = 32
|
|
307
|
+
|
|
308
|
+
def _visit_ternary(self, node: Ternary) -> str:
|
|
309
|
+
links = [node]
|
|
310
|
+
while isinstance(links[-1].false_val, Ternary):
|
|
311
|
+
links.append(links[-1].false_val)
|
|
312
|
+
string_na = self._ternary_string_na(links)
|
|
313
|
+
if len(links) < self._FLAT_TERNARY_CHAIN:
|
|
314
|
+
c = self._coerce_bool_expr(
|
|
315
|
+
self._visit_expr(node.condition), node.condition
|
|
316
|
+
)
|
|
317
|
+
t = self._ternary_arm(node.true_val, string_na)
|
|
318
|
+
f = self._ternary_arm(node.false_val, string_na)
|
|
319
|
+
return f"(({c}) ? ({t}) : ({f}))"
|
|
320
|
+
arms = []
|
|
321
|
+
for link in links:
|
|
322
|
+
c = self._coerce_bool_expr(
|
|
323
|
+
self._visit_expr(link.condition), link.condition
|
|
324
|
+
)
|
|
325
|
+
t = self._ternary_arm(link.true_val, string_na)
|
|
326
|
+
arms.append(f"({c}) ? ({t}) : ")
|
|
327
|
+
return f"({''.join(arms)}({self._ternary_arm(links[-1].false_val, string_na)}))"
|
|
328
|
+
|
|
329
|
+
def _ternary_string_na(self, links: list[Ternary]) -> bool:
|
|
330
|
+
"""A ``?:`` chain whose every value arm but a bare ``na`` is a
|
|
331
|
+
string: its ``na`` arm is the string na (``cond ? "BUY" : na``, as
|
|
332
|
+
libraries return a signal). It used to emit ``na<double>()`` beside a
|
|
333
|
+
``std::string``, which does not compile."""
|
|
334
|
+
arms = [link.true_val for link in links] + [links[-1].false_val]
|
|
335
|
+
values = [arm for arm in arms if not self._is_na_expr(arm)]
|
|
336
|
+
if len(values) == len(arms) or not values:
|
|
337
|
+
return False
|
|
338
|
+
return all(self._infer_type(arm) == "std::string" for arm in values)
|
|
339
|
+
|
|
340
|
+
def _ternary_arm(self, arm, string_na: bool) -> str:
|
|
341
|
+
if string_na and self._is_na_expr(arm):
|
|
342
|
+
return "na<std::string>()"
|
|
343
|
+
return self._visit_expr(arm)
|
|
344
|
+
|
|
243
345
|
# ------------------------------------------------------------------
|
|
244
346
|
# Target-typed RHS lowering (drawing handles are C++ structs, not doubles)
|
|
245
347
|
# ------------------------------------------------------------------
|
|
348
|
+
|
|
246
349
|
def _is_na_expr(self, node) -> bool:
|
|
247
350
|
"""True for a bare ``na`` (keyword NaLiteral or ``na`` identifier)."""
|
|
248
351
|
return (isinstance(node, NaLiteral)
|
|
@@ -341,7 +444,7 @@ class ExprVisitor:
|
|
|
341
444
|
if drawing_target is not None:
|
|
342
445
|
return f"{drawing_target}{{}}"
|
|
343
446
|
if target_cpp_type in self._udt_defs:
|
|
344
|
-
return f"{target_cpp_type}{{}}"
|
|
447
|
+
return f"{self._safe_name(target_cpp_type)}{{}}"
|
|
345
448
|
if self._is_nullable_collection_cpp_type(target_cpp_type):
|
|
346
449
|
# Maps and matrices use default construction for a typed
|
|
347
450
|
# ``na`` ID. Their ``*.new`` factories create a valid ID,
|
|
@@ -376,6 +479,30 @@ class ExprVisitor:
|
|
|
376
479
|
)
|
|
377
480
|
return self._visit_expr(value_node)
|
|
378
481
|
|
|
482
|
+
def _unpinned_read(self, node: ASTNode) -> str:
|
|
483
|
+
"""A read of a request no data is pinned for
|
|
484
|
+
(``external_requests``): the run stops with the request named, and
|
|
485
|
+
the value the expression would have is only there for its type. A
|
|
486
|
+
request lowered onto pinned data stops the run so only when its data
|
|
487
|
+
is missing (``_request_data_missing``)."""
|
|
488
|
+
notes = node.annotations
|
|
489
|
+
marker = notes[UNPINNED_ANNOTATION]
|
|
490
|
+
node.annotations = {k: v for k, v in notes.items() if k != UNPINNED_ANNOTATION}
|
|
491
|
+
try:
|
|
492
|
+
value = self._visit_expr(node)
|
|
493
|
+
finally:
|
|
494
|
+
node.annotations = notes
|
|
495
|
+
if isinstance(marker, dict):
|
|
496
|
+
# Evaluated first: a recorded request sets its flag where it is
|
|
497
|
+
# evaluated, which may be this read.
|
|
498
|
+
stop = (f'pine_runtime_error(std::string('
|
|
499
|
+
f'"{self._cpp_string_escape(marker["message"])}"))')
|
|
500
|
+
return (f"([&]() {{ auto _pf_read = {value}; "
|
|
501
|
+
f"if ({self._request_data_missing(marker['ref'])}) {stop}; "
|
|
502
|
+
f"return _pf_read; }}())")
|
|
503
|
+
return (f'([&]() {{ pine_runtime_error(std::string("{self._cpp_string_escape(marker)}")); '
|
|
504
|
+
f"return {value}; }}())")
|
|
505
|
+
|
|
379
506
|
def _visit_ident(self, node: Identifier) -> str:
|
|
380
507
|
name = node.name
|
|
381
508
|
# Bare 'na' identifier → na<double>()
|
|
@@ -406,7 +533,7 @@ class ExprVisitor:
|
|
|
406
533
|
if isinstance(val, (int, float)):
|
|
407
534
|
return str(val)
|
|
408
535
|
if isinstance(val, str):
|
|
409
|
-
return f'std::string("{val}")'
|
|
536
|
+
return f'std::string("{self._cpp_string_escape(val)}")'
|
|
410
537
|
# TA runtime-reset lowering: an input-backed var renders as its
|
|
411
538
|
# override-aware getter (not the member name), because the reset may
|
|
412
539
|
# run before the input members are initialised (evaluate_security path).
|
|
@@ -430,12 +557,11 @@ class ExprVisitor:
|
|
|
430
557
|
):
|
|
431
558
|
return "(int64_t)pine_color::black"
|
|
432
559
|
# Series var — read current value
|
|
433
|
-
safe = self._safe_name(name)
|
|
434
|
-
# Apply per-call-site var remap (for function-local vars)
|
|
435
|
-
if self._active_var_remap and safe in self._active_var_remap:
|
|
436
|
-
safe = self._active_var_remap[safe]
|
|
560
|
+
safe = self._call_site_var_name(node, self._safe_name(name))
|
|
437
561
|
if self._binding_is_series(name, safe):
|
|
438
562
|
return f"{safe}[0]"
|
|
563
|
+
if name in self._nonfinite_int_names() and name not in self._current_func_locals:
|
|
564
|
+
return self._nonfinite_int_read(safe)
|
|
439
565
|
# Safety net: by here the name resolved to none of the builtins,
|
|
440
566
|
# constants, parameters, or declared variables handled above, so
|
|
441
567
|
# ``return safe`` would emit a bare identifier that is an *undeclared
|
|
@@ -457,6 +583,14 @@ class ExprVisitor:
|
|
|
457
583
|
)
|
|
458
584
|
return safe
|
|
459
585
|
|
|
586
|
+
def _nonfinite_int_read(self, safe: str) -> str:
|
|
587
|
+
"""A read of a ``_nonfinite_int_names`` name: an infinity reads na, as
|
|
588
|
+
the int's ``na<int>()`` did, except as the operand of an ordering
|
|
589
|
+
comparison (``_visit_binop_operand``), which orders it as the number."""
|
|
590
|
+
if getattr(self, "_nonfinite_int_raw", False):
|
|
591
|
+
return safe
|
|
592
|
+
return f"(std::isfinite({safe}) ? {safe} : na<double>())"
|
|
593
|
+
|
|
460
594
|
def _ident_is_resolvable(self, name: str) -> bool:
|
|
461
595
|
"""True when a bare identifier maps to a builtin, constant, parameter,
|
|
462
596
|
or any declared variable/type/enum/function — i.e. emitting it will not
|
|
@@ -499,11 +633,16 @@ class ExprVisitor:
|
|
|
499
633
|
"""Lower an assignment target with UDT journal capture enabled."""
|
|
500
634
|
|
|
501
635
|
previous = getattr(self, "_udt_mutable_expr_depth", 0)
|
|
636
|
+
previous_target = getattr(self, "_udt_assignment_target", None)
|
|
502
637
|
self._udt_mutable_expr_depth = previous + 1
|
|
638
|
+
# A UDT array field assigned rebinds the field itself, not the array
|
|
639
|
+
# it holds (``_visit_member_access``).
|
|
640
|
+
self._udt_assignment_target = node
|
|
503
641
|
try:
|
|
504
642
|
return self._visit_expr(node)
|
|
505
643
|
finally:
|
|
506
644
|
self._udt_mutable_expr_depth = previous
|
|
645
|
+
self._udt_assignment_target = previous_target
|
|
507
646
|
|
|
508
647
|
def _visit_member_access(self, node: MemberAccess) -> str:
|
|
509
648
|
# UDT field whose type was a drawing primitive (label/line/box/
|
|
@@ -540,9 +679,18 @@ class ExprVisitor:
|
|
|
540
679
|
or mutable_collection
|
|
541
680
|
else "read"
|
|
542
681
|
)
|
|
543
|
-
|
|
682
|
+
field = f"{arena}.{access}({owner}).{self._safe_name(node.member)}"
|
|
683
|
+
if (self._udt_array_field_cpp(field_spec, owner_spec.name) is not None
|
|
684
|
+
and node is not getattr(self, "_udt_assignment_target", None)):
|
|
685
|
+
# A ``_PFArrayField<T>``: every read, element write and method
|
|
686
|
+
# call reaches the array it holds.
|
|
687
|
+
return f"(*{field})"
|
|
688
|
+
return field
|
|
544
689
|
if isinstance(node.object, Identifier):
|
|
545
690
|
ns = node.object.name
|
|
691
|
+
if ns == "alert" and node.member in ALERT_FREQ_VALUES:
|
|
692
|
+
# TradingView's const string values (its own tape spells them).
|
|
693
|
+
return f'std::string("{ALERT_FREQ_VALUES[node.member]}")'
|
|
546
694
|
if ns == "strategy":
|
|
547
695
|
# Direction constants
|
|
548
696
|
if node.member == "long":
|
|
@@ -654,7 +802,7 @@ class ExprVisitor:
|
|
|
654
802
|
if ns == "ta":
|
|
655
803
|
if node.member == "tr":
|
|
656
804
|
return ("(std::isnan(_s_close[1]) ? "
|
|
657
|
-
"(
|
|
805
|
+
"na<double>() : "
|
|
658
806
|
"std::max(current_bar_.high - current_bar_.low, "
|
|
659
807
|
"std::max(std::abs(current_bar_.high - _s_close[1]), "
|
|
660
808
|
"std::abs(current_bar_.low - _s_close[1]))))")
|
|
@@ -685,7 +833,6 @@ class ExprVisitor:
|
|
|
685
833
|
for _i, site in _candidates:
|
|
686
834
|
if _i is None or _i in self._dead_ta_indices:
|
|
687
835
|
continue
|
|
688
|
-
ta_short = site.class_name.split("::")[-1].lower()
|
|
689
836
|
if site.member_name.startswith(f"_ta_{node.member}_"):
|
|
690
837
|
if node.member == "vwap":
|
|
691
838
|
# Same implicit tail as TA_IMPLICIT_APPEND["vwap"]:
|
|
@@ -794,7 +941,7 @@ class ExprVisitor:
|
|
|
794
941
|
if node.member == "islast":
|
|
795
942
|
return "barstate_islast_"
|
|
796
943
|
if node.member == "isnew":
|
|
797
|
-
return "
|
|
944
|
+
return "is_first_tick()"
|
|
798
945
|
if node.member == "isconfirmed":
|
|
799
946
|
return "is_last_tick_"
|
|
800
947
|
if node.member == "ishistory":
|
|
@@ -813,6 +960,11 @@ class ExprVisitor:
|
|
|
813
960
|
if ns == "adjustment":
|
|
814
961
|
# adjustment.none / dividends / splits. Unknown member -> "none" (0).
|
|
815
962
|
return ADJUSTMENT_MAP.get(node.member, "0")
|
|
963
|
+
if ns == "barmerge":
|
|
964
|
+
# A barmerge constant held or compared as a value (the
|
|
965
|
+
# analyzer types it int): on 1, off 0. A request's gaps or
|
|
966
|
+
# lookahead reads the constant as written.
|
|
967
|
+
return "1" if node.member in ("gaps_on", "lookahead_on") else "0"
|
|
816
968
|
if ns == "dayofweek":
|
|
817
969
|
return DAYOFWEEK_MAP.get(node.member, "0")
|
|
818
970
|
if ns == "session":
|
|
@@ -820,20 +972,74 @@ class ExprVisitor:
|
|
|
820
972
|
return 'std::string("regular")'
|
|
821
973
|
if node.member == "extended":
|
|
822
974
|
return 'std::string("extended")'
|
|
823
|
-
# session.
|
|
824
|
-
#
|
|
825
|
-
#
|
|
826
|
-
#
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
if node.member
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
975
|
+
# session.isfirstbar / islastbar mark the chart's session day,
|
|
976
|
+
# which the engine widens by the pre- and post-market bars an
|
|
977
|
+
# extended-hours chart holds, and session.isfirstbar_regular /
|
|
978
|
+
# islastbar_regular the regular session's day: the host's
|
|
979
|
+
# session_is*bar_ and session_is*bar_regular_ facts
|
|
980
|
+
# (tests/test_e2e_session_ismarket.py).
|
|
981
|
+
if node.member in ("ismarket", "ispremarket", "ispostmarket"):
|
|
982
|
+
# A chart bar is in market when the kernel's in-session
|
|
983
|
+
# fact holds it (session_ismarket_): the bar read at its
|
|
984
|
+
# interval's first eligible instant, so a bar that opens
|
|
985
|
+
# in a break and holds the reopen is in market, as
|
|
986
|
+
# TradingView's TSE:7203 and CBOT:ZC1! 60-minute tapes
|
|
987
|
+
# flag it, and a D/W/M bar is
|
|
988
|
+
# (tests/test_e2e_session_windows.py). A run without a
|
|
989
|
+
# timeframe gets no such fact (script_tf_ is empty); there
|
|
990
|
+
# the session calendar answers at the bar's open
|
|
991
|
+
# (codegen/session_market.py). The time-of-day predicates
|
|
992
|
+
# read the instant alone: ismarket's reads a bar that
|
|
993
|
+
# opens in a break out of market, and until engine lane
|
|
994
|
+
# W11-ENG-TIME-COLOR it also missed the Sunday-evening
|
|
995
|
+
# open of a ":23456" session and every "0000-2400" bar
|
|
996
|
+
# (tests/test_e2e_session_ismarket.py).
|
|
997
|
+
# A bar in market is in neither extended session; off it,
|
|
998
|
+
# the engine's windows decide. A request.security payload
|
|
999
|
+
# runs on its own bars, whose timeframe the chart's facts
|
|
1000
|
+
# do not describe, so it keeps the predicates at the
|
|
1001
|
+
# security bar's time, with a warning.
|
|
1002
|
+
args = "(syminfo_.session, syminfo_.timezone, current_bar_.timestamp)"
|
|
1003
|
+
predicate = f"pine_session_{node.member}{args}"
|
|
1004
|
+
if self._security_payload_depth:
|
|
1005
|
+
if id(node) not in self._warned_security_session_sites:
|
|
1006
|
+
self._warned_security_session_sites.add(id(node))
|
|
1007
|
+
# A bar that opens in a break and holds the reopen
|
|
1008
|
+
# is in market (the chart's reading); pre- and
|
|
1009
|
+
# post-market read a break as neither. A D/W/M bar
|
|
1010
|
+
# holds whole session days, where the predicate
|
|
1011
|
+
# reads its open's time of day.
|
|
1012
|
+
daily = {"ismarket": "always in market",
|
|
1013
|
+
"ispremarket": "never pre-market",
|
|
1014
|
+
"ispostmarket": "never post-market"}[node.member]
|
|
1015
|
+
opens = ("a bar that opens in a session break and holds "
|
|
1016
|
+
"the reopen reads as out of market, and "
|
|
1017
|
+
if node.member == "ismarket" else "")
|
|
1018
|
+
self._codegen_warning(
|
|
1019
|
+
node,
|
|
1020
|
+
f"session.{node.member} inside request.security keeps "
|
|
1021
|
+
"the time-of-day predicate at the security bar's open "
|
|
1022
|
+
f"time, which can differ from TradingView: {opens}"
|
|
1023
|
+
f"a D/W/M bar, {daily} on TradingView, reads its "
|
|
1024
|
+
"open's time of day. The chart's own session flags "
|
|
1025
|
+
"read the engine's session facts.",
|
|
1026
|
+
)
|
|
1027
|
+
return predicate
|
|
1028
|
+
self._uses_session_market = True
|
|
1029
|
+
ismarket = ("(script_tf_.empty() ? _pf_session_market_(syminfo_.session, "
|
|
1030
|
+
"syminfo_.timezone, script_tf_, current_bar_.timestamp) "
|
|
1031
|
+
": session_ismarket_)")
|
|
1032
|
+
if node.member == "ismarket":
|
|
1033
|
+
return ismarket
|
|
1034
|
+
return f"(!{ismarket} && {predicate})"
|
|
1035
|
+
if node.member == "isfirstbar":
|
|
834
1036
|
return "session_isfirstbar_"
|
|
835
|
-
if node.member
|
|
1037
|
+
if node.member == "islastbar":
|
|
836
1038
|
return "session_islastbar_"
|
|
1039
|
+
if node.member == "isfirstbar_regular":
|
|
1040
|
+
return "session_isfirstbar_regular_"
|
|
1041
|
+
if node.member == "islastbar_regular":
|
|
1042
|
+
return "session_islastbar_regular_"
|
|
837
1043
|
return "false"
|
|
838
1044
|
if ns == "syminfo":
|
|
839
1045
|
_syminfo = SYMINFO_MEMBER_MAP.get(node.member)
|
|
@@ -916,7 +1122,10 @@ class ExprVisitor:
|
|
|
916
1122
|
if name in self._enum_defs:
|
|
917
1123
|
members = self._enum_defs[name]
|
|
918
1124
|
if node.member in members:
|
|
919
|
-
return
|
|
1125
|
+
return (
|
|
1126
|
+
f"{self._safe_name(name)}_"
|
|
1127
|
+
f"{self._safe_name(node.member)}"
|
|
1128
|
+
)
|
|
920
1129
|
|
|
921
1130
|
# Unknown member access — emit as string constant (e.g., enum values)
|
|
922
1131
|
obj = self._visit_expr(node.object)
|
|
@@ -935,9 +1144,9 @@ class ExprVisitor:
|
|
|
935
1144
|
or name in self._current_func_locals
|
|
936
1145
|
or name in self._udt_var_types
|
|
937
1146
|
):
|
|
938
|
-
safe = self.
|
|
939
|
-
|
|
940
|
-
|
|
1147
|
+
safe = self._call_site_var_name(
|
|
1148
|
+
node.object, self._safe_name(name)
|
|
1149
|
+
)
|
|
941
1150
|
return f"{safe}.{node.member}"
|
|
942
1151
|
if name not in self.ctx.series_vars:
|
|
943
1152
|
# Unknown identifier — likely an enum value
|
|
@@ -1026,6 +1235,23 @@ class ExprVisitor:
|
|
|
1026
1235
|
f"({compare}); }}())"
|
|
1027
1236
|
)
|
|
1028
1237
|
|
|
1238
|
+
def _relational_wrapper(self, op: str, left_node, right_node) -> str | None:
|
|
1239
|
+
"""The wrapper ``_lower_relational`` gives a relational: ``"float"``
|
|
1240
|
+
(the fixed-band comparator), ``"int"`` (the KI-71 na comparator) or
|
|
1241
|
+
None (the plain C++ operator). Both wrappers bind each operand once,
|
|
1242
|
+
the left one first."""
|
|
1243
|
+
if op in _RELATIONAL_OPS:
|
|
1244
|
+
lt = self._infer_type(left_node)
|
|
1245
|
+
rt = self._infer_type(right_node)
|
|
1246
|
+
if lt in _NA_SCALAR_CPP and rt in _NA_SCALAR_CPP:
|
|
1247
|
+
if "double" in (lt, rt):
|
|
1248
|
+
return "float"
|
|
1249
|
+
lk = self._operand_na_kind(left_node, lt)
|
|
1250
|
+
rk = self._operand_na_kind(right_node, rt)
|
|
1251
|
+
if "int" in (lk, rk):
|
|
1252
|
+
return "int"
|
|
1253
|
+
return None
|
|
1254
|
+
|
|
1029
1255
|
def _lower_relational(self, op: str, left_node, right_node,
|
|
1030
1256
|
left_cpp: str, right_cpp: str) -> str:
|
|
1031
1257
|
"""Lower a Pine relational with Pine's na and float-precision rules.
|
|
@@ -1037,17 +1263,11 @@ class ExprVisitor:
|
|
|
1037
1263
|
KI-71 wrapper only when an operand can carry the INT_MIN ``na`` sentinel.
|
|
1038
1264
|
Non-relational operators and nonnumeric operands fall through unchanged.
|
|
1039
1265
|
"""
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
return self._emit_float_relational(op, left_cpp, right_cpp)
|
|
1046
|
-
lk = self._operand_na_kind(left_node, lt)
|
|
1047
|
-
rk = self._operand_na_kind(right_node, rt)
|
|
1048
|
-
int_na = "int" in (lk, rk)
|
|
1049
|
-
if int_na:
|
|
1050
|
-
return self._emit_na_relational(op, left_cpp, right_cpp)
|
|
1266
|
+
wrapper = self._relational_wrapper(op, left_node, right_node)
|
|
1267
|
+
if wrapper == "float":
|
|
1268
|
+
return self._emit_float_relational(op, left_cpp, right_cpp)
|
|
1269
|
+
if wrapper == "int":
|
|
1270
|
+
return self._emit_na_relational(op, left_cpp, right_cpp)
|
|
1051
1271
|
return f"({left_cpp} {op} {right_cpp})"
|
|
1052
1272
|
|
|
1053
1273
|
def _visit_binop(self, node: BinOp) -> str:
|
|
@@ -1056,12 +1276,237 @@ class ExprVisitor:
|
|
|
1056
1276
|
# C++ ``int`` literal arithmetic would wrap it. Fold it here and spell
|
|
1057
1277
|
# the value as a 64-bit literal. In-range trees are emitted as before.
|
|
1058
1278
|
folded = self._pure_int_literal_value(node)
|
|
1059
|
-
if folded is not None and not self._int_fits_int32(folded)
|
|
1279
|
+
if (folded is not None and not self._int_fits_int32(folded)
|
|
1280
|
+
and self._int_fits_int64(folded)):
|
|
1060
1281
|
return f"static_cast<int64_t>({folded}LL)"
|
|
1061
|
-
|
|
1062
|
-
|
|
1282
|
+
if node.op in ("%", "/"):
|
|
1283
|
+
# Both lower through doubles, which hold an int product's exact
|
|
1284
|
+
# value: compute a product operand in 64 bits (``_lower_binop``).
|
|
1285
|
+
for operand in (node.left, node.right):
|
|
1286
|
+
if isinstance(operand, BinOp) and operand.op == "*":
|
|
1287
|
+
self._wide_int_products.add(id(operand))
|
|
1288
|
+
left = self._visit_binop_operand(node.left, node.op)
|
|
1289
|
+
right = self._visit_binop_operand(node.right, node.op)
|
|
1290
|
+
if (self._int_arith_leaves_int32(node)
|
|
1291
|
+
and self._fold_int32_overflow_cpp(node.op, left, right) is None):
|
|
1292
|
+
# Both operands are 32-bit C++ ints and the value can leave
|
|
1293
|
+
# int32: Pine's int is 64-bit (``days * 86400000``). Operands the
|
|
1294
|
+
# C++ spells as int literals keep their fold (``_lower_binop``).
|
|
1295
|
+
return self._left_operand_first(
|
|
1296
|
+
node, left, right,
|
|
1297
|
+
lambda left, right: self._wide_int_arith_cpp(
|
|
1298
|
+
node, left, right,
|
|
1299
|
+
lambda left, right: self._lower_binop(node, left, right, widened=True)),
|
|
1300
|
+
)
|
|
1301
|
+
return self._left_operand_first(
|
|
1302
|
+
node, left, right,
|
|
1303
|
+
lambda left, right: self._lower_binop(node, left, right),
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
def _visit_binop_operand(self, operand, op: str) -> str:
|
|
1307
|
+
"""An operand's C++. An ordering comparison reads a
|
|
1308
|
+
``_nonfinite_int_names`` name's infinity as the number TradingView
|
|
1309
|
+
orders it by (``_nonfinite_int_read``)."""
|
|
1310
|
+
if (op in self.NONFINITE_INT_ORDERING_OPS
|
|
1311
|
+
and isinstance(operand, Identifier)
|
|
1312
|
+
and operand.name in self._nonfinite_int_names()):
|
|
1313
|
+
previous = getattr(self, "_nonfinite_int_raw", False)
|
|
1314
|
+
self._nonfinite_int_raw = True
|
|
1315
|
+
try:
|
|
1316
|
+
return self._visit_expr(operand)
|
|
1317
|
+
finally:
|
|
1318
|
+
self._nonfinite_int_raw = previous
|
|
1319
|
+
return self._visit_expr(operand)
|
|
1320
|
+
|
|
1321
|
+
def _left_operand_first(self, node: BinOp, left: str, right: str, lower) -> str:
|
|
1322
|
+
"""``lower(left, right)``: the C++ of ``node`` over its rendered
|
|
1323
|
+
operands, with the left one evaluated first when
|
|
1324
|
+
``_binop_operands_need_order(node)``.
|
|
1325
|
+
|
|
1326
|
+
Pine evaluates the left operand first. C++ leaves the order of the
|
|
1327
|
+
operands of + - * / % and of an overloaded operator (std::string's
|
|
1328
|
+
+ and ==, std::fmod's arguments) unspecified; GCC on x86-64 evaluates
|
|
1329
|
+
std::string's operator+ right operand first. Bind the left operand's
|
|
1330
|
+
value (a bool as bool: a std::vector<bool> element reads through a
|
|
1331
|
+
proxy), then evaluate the right operand. Shared by ``_visit_binop``
|
|
1332
|
+
and the ``request.security`` expression builder."""
|
|
1333
|
+
if not self._binop_operands_need_order(node):
|
|
1334
|
+
return lower(left, right)
|
|
1335
|
+
occupied = f"{left}\n{right}"
|
|
1336
|
+
counter = getattr(self, "_binop_lhs_counter", 0)
|
|
1337
|
+
while True:
|
|
1338
|
+
token = f"__pf_binop_lhs_{counter}"
|
|
1339
|
+
counter += 1
|
|
1340
|
+
if token not in occupied:
|
|
1341
|
+
break
|
|
1342
|
+
self._binop_lhs_counter = counter
|
|
1343
|
+
decl = "bool" if self._infer_type(node.left) == "bool" else "auto"
|
|
1344
|
+
return f"[&]{{ {decl} {token} = ({left}); return {lower(token, right)}; }}()"
|
|
1345
|
+
|
|
1346
|
+
def _binop_operands_need_order(self, node: BinOp) -> bool:
|
|
1347
|
+
"""Whether the C++ of ``node`` must evaluate its left operand first:
|
|
1348
|
+
one operand has an effect (``_expr_has_ordered_effect``) the other
|
|
1349
|
+
one can observe (it is not ``_binop_operand_is_order_free``), and the
|
|
1350
|
+
lowering does not already order them (``&&`` / ``||`` and the
|
|
1351
|
+
relational wrappers do)."""
|
|
1352
|
+
if node.op in ("and", "or"):
|
|
1353
|
+
return False
|
|
1354
|
+
if not (
|
|
1355
|
+
(self._expr_has_ordered_effect(node.left)
|
|
1356
|
+
and not self._binop_operand_is_order_free(node.right))
|
|
1357
|
+
or (self._expr_has_ordered_effect(node.right)
|
|
1358
|
+
and not self._binop_operand_is_order_free(node.left))
|
|
1359
|
+
):
|
|
1360
|
+
return False
|
|
1361
|
+
return self._relational_wrapper(node.op, node.left, node.right) is None
|
|
1362
|
+
|
|
1363
|
+
def _binop_operand_is_order_free(self, node) -> bool:
|
|
1364
|
+
"""Whether no effect of the other operand can change ``node``'s value:
|
|
1365
|
+
a literal, a variable (a Pine function cannot assign a script
|
|
1366
|
+
variable, and no collection is an operand of a binary operator), or a
|
|
1367
|
+
``str.*`` / ``math.*`` / ``color.*`` / ``ta.*`` call, ``nz`` / ``na``
|
|
1368
|
+
/ a cast, or a unary, binary or conditional operation over such
|
|
1369
|
+
operands (a TA call's state is its call site's own)."""
|
|
1370
|
+
pending = [node]
|
|
1371
|
+
while pending:
|
|
1372
|
+
current = pending.pop()
|
|
1373
|
+
if isinstance(current, (*_ORDER_FREE_LITERALS, Identifier)):
|
|
1374
|
+
continue
|
|
1375
|
+
if isinstance(current, UnaryOp):
|
|
1376
|
+
pending.append(current.operand)
|
|
1377
|
+
elif isinstance(current, BinOp):
|
|
1378
|
+
pending.extend((current.left, current.right))
|
|
1379
|
+
elif isinstance(current, Ternary):
|
|
1380
|
+
pending.extend(
|
|
1381
|
+
(current.condition, current.true_val, current.false_val)
|
|
1382
|
+
)
|
|
1383
|
+
elif isinstance(current, FuncCall) and self._call_is_order_free(current):
|
|
1384
|
+
pending.extend(current.args)
|
|
1385
|
+
pending.extend(current.kwargs.values())
|
|
1386
|
+
else:
|
|
1387
|
+
return False
|
|
1388
|
+
return True
|
|
1389
|
+
|
|
1390
|
+
def _call_is_order_free(self, node: FuncCall) -> bool:
|
|
1391
|
+
callee = node.callee
|
|
1392
|
+
if isinstance(callee, Identifier):
|
|
1393
|
+
return (callee.name in _ORDER_FREE_CALLS
|
|
1394
|
+
and callee.name not in self._func_info_map)
|
|
1395
|
+
return (isinstance(callee, MemberAccess)
|
|
1396
|
+
and isinstance(callee.object, Identifier)
|
|
1397
|
+
and callee.object.name in _ORDER_FREE_CALL_NAMESPACES)
|
|
1398
|
+
|
|
1399
|
+
def _expr_has_ordered_effect(self, node) -> bool:
|
|
1400
|
+
"""Whether evaluating ``node`` changes what another operand of the same
|
|
1401
|
+
expression can read, or does something whose order is observable: it
|
|
1402
|
+
mutates an array, map or matrix (``COLLECTION_MUTATING_METHODS``),
|
|
1403
|
+
creates, changes or deletes a drawing, places or cancels an order,
|
|
1404
|
+
writes a log line or an alert, stops the script, or calls a user
|
|
1405
|
+
function or method that does one of these or assigns a UDT field.
|
|
1406
|
+
|
|
1407
|
+
TA state and ``var`` state belong to their own call site and
|
|
1408
|
+
``math.random`` draws from its call site's own stream, so none of
|
|
1409
|
+
them is such an effect. A method call is judged by its name alone
|
|
1410
|
+
(every UDT method of that name counts), since the receiver's type is
|
|
1411
|
+
not resolved here.
|
|
1412
|
+
|
|
1413
|
+
The walk is iterative (it adds no recursion depth to the visitor
|
|
1414
|
+
that asks) and reuses the answer of every expression asked before:
|
|
1415
|
+
an operator chain asks each of its operands once."""
|
|
1416
|
+
memo = getattr(self, "_ordered_effect_memo", None)
|
|
1417
|
+
if memo is None:
|
|
1418
|
+
memo = self._ordered_effect_memo = {}
|
|
1419
|
+
hit = memo.get(id(node))
|
|
1420
|
+
if hit is not None and hit[0] is node:
|
|
1421
|
+
return hit[1]
|
|
1422
|
+
result = False
|
|
1423
|
+
pending = [node]
|
|
1424
|
+
while pending:
|
|
1425
|
+
current = pending.pop()
|
|
1426
|
+
hit = memo.get(id(current))
|
|
1427
|
+
if hit is not None and hit[0] is current:
|
|
1428
|
+
if hit[1]:
|
|
1429
|
+
result = True
|
|
1430
|
+
break
|
|
1431
|
+
continue
|
|
1432
|
+
if (isinstance(current, FuncCall)
|
|
1433
|
+
and self._call_has_ordered_effect(current)) or (
|
|
1434
|
+
isinstance(current, Assignment)
|
|
1435
|
+
and isinstance(current.target, MemberAccess)):
|
|
1436
|
+
result = True
|
|
1437
|
+
break
|
|
1438
|
+
pending.extend(_ast_children(current))
|
|
1439
|
+
memo[id(node)] = (node, result)
|
|
1440
|
+
return result
|
|
1441
|
+
|
|
1442
|
+
def _call_has_ordered_effect(self, node: FuncCall) -> bool:
|
|
1443
|
+
"""The call itself (not its arguments) is an ordered effect."""
|
|
1444
|
+
callee = node.callee
|
|
1445
|
+
if isinstance(callee, Identifier):
|
|
1446
|
+
if callee.name in self._func_info_map:
|
|
1447
|
+
return self._callable_has_ordered_effect(callee.name)
|
|
1448
|
+
return callee.name == "alert"
|
|
1449
|
+
if not isinstance(callee, MemberAccess):
|
|
1450
|
+
return False
|
|
1451
|
+
member = callee.member
|
|
1452
|
+
owner = callee.object
|
|
1453
|
+
if member in COLLECTION_MUTATING_METHODS:
|
|
1454
|
+
return True
|
|
1455
|
+
if isinstance(owner, Identifier):
|
|
1456
|
+
if (owner.name, member) in _ORDERED_EFFECT_CALLS:
|
|
1457
|
+
return True
|
|
1458
|
+
if (owner.name in _DRAWING_EFFECT_NAMESPACES
|
|
1459
|
+
and not member.startswith("get_")):
|
|
1460
|
+
return True
|
|
1461
|
+
if (isinstance(owner, MemberAccess) and owner.member == "risk"
|
|
1462
|
+
and isinstance(owner.object, Identifier)
|
|
1463
|
+
and owner.object.name == "strategy"):
|
|
1464
|
+
return True
|
|
1465
|
+
if (member.startswith(("set_", "cell_set_"))
|
|
1466
|
+
or member in _DRAWING_EFFECT_METHODS):
|
|
1467
|
+
return True
|
|
1468
|
+
methods = getattr(self, "_udt_method_keys_by_name", None)
|
|
1469
|
+
if methods is None:
|
|
1470
|
+
methods = self._udt_method_keys_by_name = {}
|
|
1471
|
+
for key, info in self._func_info_map.items():
|
|
1472
|
+
if getattr(info, "is_udt_method", False):
|
|
1473
|
+
methods.setdefault(key.rsplit(".", 1)[-1], []).append(key)
|
|
1474
|
+
return any(
|
|
1475
|
+
self._callable_has_ordered_effect(key)
|
|
1476
|
+
for key in methods.get(member, ())
|
|
1477
|
+
)
|
|
1478
|
+
|
|
1479
|
+
def _callable_has_ordered_effect(self, key: str) -> bool:
|
|
1480
|
+
"""Whether a user function or method's body has an ordered effect."""
|
|
1481
|
+
memo = getattr(self, "_callable_effect_memo", None)
|
|
1482
|
+
if memo is None:
|
|
1483
|
+
memo = self._callable_effect_memo = {}
|
|
1484
|
+
if key not in memo:
|
|
1485
|
+
memo[key] = False # while its body is walked
|
|
1486
|
+
func_node = getattr(self._func_info_map.get(key), "node", None)
|
|
1487
|
+
memo[key] = func_node is not None and any(
|
|
1488
|
+
self._expr_has_ordered_effect(stmt) for stmt in func_node.body
|
|
1489
|
+
)
|
|
1490
|
+
return memo[key]
|
|
1491
|
+
|
|
1492
|
+
def _lower_binop(self, node: BinOp, left: str, right: str, widened: bool = False) -> str:
|
|
1493
|
+
"""The C++ of ``node`` over its rendered operands; ``widened`` when
|
|
1494
|
+
``_visit_binop`` computes it in 64 bits already (``left`` cast)."""
|
|
1063
1495
|
cpp_ops = {"and": "&&", "or": "||"}
|
|
1064
1496
|
op = cpp_ops.get(node.op, node.op)
|
|
1497
|
+
if node.op in ("and", "or"):
|
|
1498
|
+
# Pine converts numeric operands to two-state booleans before
|
|
1499
|
+
# applying short-circuit logic. C++ treats NaN and INT_MIN as
|
|
1500
|
+
# true, so both operands need the same na-aware truthiness rule.
|
|
1501
|
+
left = self._coerce_bool_expr(left, node.left)
|
|
1502
|
+
right = self._coerce_bool_expr(right, node.right)
|
|
1503
|
+
if self._pine_v5_body:
|
|
1504
|
+
# v5 evaluates both operands, left then right, whatever the
|
|
1505
|
+
# left one decides (TradingView: a stateful right operand
|
|
1506
|
+
# runs on every bar).
|
|
1507
|
+
return (f"([&]() -> bool {{ const bool _pf_v5_l = ({left}); "
|
|
1508
|
+
f"const bool _pf_v5_r = ({right}); "
|
|
1509
|
+
f"return _pf_v5_l {op} _pf_v5_r; }}())")
|
|
1065
1510
|
if node.op == "+":
|
|
1066
1511
|
lt = self._infer_type(node.left)
|
|
1067
1512
|
rt = self._infer_type(node.right)
|
|
@@ -1083,37 +1528,244 @@ class ExprVisitor:
|
|
|
1083
1528
|
# Ref: https://www.tradingview.com/pine-script-docs/concepts/operators/
|
|
1084
1529
|
if node.op == "/":
|
|
1085
1530
|
return f"((double)({left}) / (double)({right}))"
|
|
1531
|
+
if node.op in ("==", "!=") and self._pine_v5_body:
|
|
1532
|
+
self._refuse_v5_bool_na_observer(
|
|
1533
|
+
node, f"'{node.op}'", (node.left, node.right))
|
|
1534
|
+
# Operands that render as C++ ``int`` literal arithmetic although
|
|
1535
|
+
# the tree above did not fold (a name spelled otherwise than
|
|
1536
|
+
# ``_inlined_int_constant`` reads it): a result beyond int32 is a
|
|
1537
|
+
# 64-bit Pine int, never a C++ overflow.
|
|
1538
|
+
folded_cpp = self._fold_int32_overflow_cpp(node.op, left, right)
|
|
1539
|
+
if folded_cpp is not None:
|
|
1540
|
+
return folded_cpp
|
|
1541
|
+
if (node.op == "*" and not widened and id(node) in self._wide_int_products
|
|
1542
|
+
and self._pure_int_literal_value(node) is None
|
|
1543
|
+
and self._emits_int32(node.left) and self._emits_int32(node.right)):
|
|
1544
|
+
# Pine's int is 64-bit. A product a ``%`` or ``/`` reads reaches a
|
|
1545
|
+
# double whole, where the C++ ``int`` product wrapped: the
|
|
1546
|
+
# Park-Miller step ``(s * 48271) % 2147483647``
|
|
1547
|
+
# (``fixtures/tail_f_tv/int_product``). A product that can leave
|
|
1548
|
+
# int32 by its operands' bounds is ``widened`` already, na-aware
|
|
1549
|
+
# (``_visit_binop``, ``_wide_int_arith_cpp``), and keeps that
|
|
1550
|
+
# spelling; this covers the rest a ``%`` or ``/`` reads (an
|
|
1551
|
+
# ``array.get`` operand, which the bounds do not know).
|
|
1552
|
+
return f"((int64_t)({left}) * ({right}))"
|
|
1086
1553
|
return self._lower_relational(op, node.left, node.right, left, right)
|
|
1087
1554
|
|
|
1555
|
+
def _emits_int32(self, node) -> bool:
|
|
1556
|
+
"""Whether ``node`` is emitted as a C++ ``int`` for certain: an int
|
|
1557
|
+
literal, a name stored as ``int``, an element of an ``array<int>``
|
|
1558
|
+
(read or iterated), or a sign, sum or difference of those."""
|
|
1559
|
+
if isinstance(node, NumberLiteral):
|
|
1560
|
+
return isinstance(node.value, int) and not isinstance(node.value, bool)
|
|
1561
|
+
if isinstance(node, UnaryOp) and node.op in ("-", "+"):
|
|
1562
|
+
return self._emits_int32(node.operand)
|
|
1563
|
+
if isinstance(node, BinOp) and node.op in ("+", "-"):
|
|
1564
|
+
return self._emits_int32(node.left) and self._emits_int32(node.right)
|
|
1565
|
+
if isinstance(node, Identifier):
|
|
1566
|
+
if node.name in getattr(self, "_current_loop_vars", set()):
|
|
1567
|
+
# A ``for ... in`` element: an int only over an ``array<int>``.
|
|
1568
|
+
spec = getattr(self, "_current_loop_var_specs", {}).get(node.name)
|
|
1569
|
+
return (spec is not None and spec.kind == "primitive"
|
|
1570
|
+
and self._type_spec_to_cpp(spec) == "int")
|
|
1571
|
+
return (not self._emitted_value_is_double(node)
|
|
1572
|
+
and self._slot_scalar_cpp_type(node.name) == "int"
|
|
1573
|
+
and self._infer_type(node) == "int")
|
|
1574
|
+
if isinstance(node, FuncCall):
|
|
1575
|
+
callee = node.callee
|
|
1576
|
+
if not isinstance(callee, MemberAccess) or callee.member != "get":
|
|
1577
|
+
return False
|
|
1578
|
+
receiver = (callee.object if not (
|
|
1579
|
+
isinstance(callee.object, Identifier) and callee.object.name == "array")
|
|
1580
|
+
else (node.args[0] if node.args else None))
|
|
1581
|
+
if not isinstance(receiver, Identifier):
|
|
1582
|
+
return False
|
|
1583
|
+
spec = self._collection_spec_for_name(receiver.name)
|
|
1584
|
+
return (spec is not None and spec.kind == "array" and spec.element is not None
|
|
1585
|
+
and self._type_spec_to_cpp(spec.element) == "int")
|
|
1586
|
+
return False
|
|
1587
|
+
|
|
1588
|
+
def _refuse_v5_bool_na_observer(self, node, what: str, operands) -> None:
|
|
1589
|
+
"""A v5 bool can be na (a comparison with na, an na literal, a bool's
|
|
1590
|
+
history before the first bar); PineForge's bool cannot. It reads as
|
|
1591
|
+
false wherever v5 casts it to a bool (TradingView's tape of
|
|
1592
|
+
``xc_v5_lib``), but ``na()``, ``nz()``, ``fixnan()``,
|
|
1593
|
+
``str.tostring``/``str.format`` and ``==``/``!=`` of a bool tell the
|
|
1594
|
+
third state apart (a v5 probe's tape outside this repository:
|
|
1595
|
+
``na(b)`` true, ``b == false`` na)."""
|
|
1596
|
+
if any(self._infer_type(o) == "bool" for o in operands if o is not None):
|
|
1597
|
+
library = (getattr(node.loc, "file", None) or "a v5 library")
|
|
1598
|
+
self._codegen_error(
|
|
1599
|
+
node,
|
|
1600
|
+
f"library '{library}' is //@version=5: {what} of a bool reads "
|
|
1601
|
+
"v5's third bool state (na), which PineForge's two-state bool "
|
|
1602
|
+
"does not keep",
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1088
1605
|
def _visit_unaryop(self, node: UnaryOp) -> str:
|
|
1089
1606
|
operand = self._visit_expr(node.operand)
|
|
1090
1607
|
if node.op == "not":
|
|
1091
|
-
return f"!({operand})"
|
|
1092
|
-
return
|
|
1608
|
+
return f"!({self._coerce_bool_expr(operand, node.operand)})"
|
|
1609
|
+
return unary_sign_cpp(node.op, operand)
|
|
1610
|
+
|
|
1611
|
+
def _visit_session_history(self, node: Subscript, series_idx: str) -> str:
|
|
1612
|
+
"""``session.<flag>[k]``: the value the flag had k bars ago at the top
|
|
1613
|
+
level of the script, k calls ago in a function body.
|
|
1614
|
+
|
|
1615
|
+
TradingView's tapes (``tests/test_e2e_session_history.py``) read a
|
|
1616
|
+
top-level offset by bars, in a block and on a lazy operand too, and a
|
|
1617
|
+
function body's by the calls of its call site, whichever operand or
|
|
1618
|
+
block holds the read. A top-level read indexes the flag's Series,
|
|
1619
|
+
pushed on every chart bar (``_prescan_session_history``); a function
|
|
1620
|
+
body's indexes its emitted call site's Series, pushed at the function's
|
|
1621
|
+
entry (``_prepare_inline_history_members``). A request.security
|
|
1622
|
+
expression that reaches the read through its own operators builds it
|
|
1623
|
+
on the requested clock (security.py); one that reaches it through a
|
|
1624
|
+
call's argument or a variable has no such history here and is refused
|
|
1625
|
+
at the read. So is a read emitted in a function the analyzer lists in
|
|
1626
|
+
``session_history_unsafe`` (a method, or a function a method, a
|
|
1627
|
+
request.security expression or a UDT field default reaches), and one
|
|
1628
|
+
with no registered Series: never shared state. The refusal waits for
|
|
1629
|
+
the C++ (``_settle_session_reads``): a read in an argument the codegen
|
|
1630
|
+
renders and leaves out refuses nothing. A read in a function this
|
|
1631
|
+
analysis did not clone (``session_uncloned``) asks for its clones
|
|
1632
|
+
instead.
|
|
1633
|
+
"""
|
|
1634
|
+
flag = node.object.member
|
|
1635
|
+
where = next((n for n in (node, node.index, node.object)
|
|
1636
|
+
if getattr(n, "loc", None) is not None), node)
|
|
1637
|
+
if self._security_payload_depth:
|
|
1638
|
+
return self._refused_session_read(
|
|
1639
|
+
node, where,
|
|
1640
|
+
f"session.{flag}[...] cannot be read here: PineForge keeps a session "
|
|
1641
|
+
"flag's history on the requested clock only for a read the "
|
|
1642
|
+
"request.security expression reaches through its own operators, "
|
|
1643
|
+
"not through a call's argument, a function or a variable.",
|
|
1644
|
+
)
|
|
1645
|
+
owner = self._session_call_owner.get(id(node))
|
|
1646
|
+
if owner is not None:
|
|
1647
|
+
why = (getattr(self.ctx, "session_history_unsafe", None) or {}).get(owner)
|
|
1648
|
+
if why:
|
|
1649
|
+
return self._refused_session_read(
|
|
1650
|
+
node, where,
|
|
1651
|
+
f"session.{flag}[...] cannot be read in {owner.split('.')[-1]}(), "
|
|
1652
|
+
f"{why}: PineForge keeps a session flag's history in a function by "
|
|
1653
|
+
"each of its call sites, and cannot tell this one apart.",
|
|
1654
|
+
)
|
|
1655
|
+
member = self._inline_history_member_by_key.get(
|
|
1656
|
+
("session_call", owner, flag, self._current_instance_name))
|
|
1657
|
+
if member is not None:
|
|
1658
|
+
return f"{member}[{series_idx}]"
|
|
1659
|
+
elif flag in self._session_history_flags:
|
|
1660
|
+
return f"{self._session_history_member(flag)}[{series_idx}]"
|
|
1661
|
+
uncloned = getattr(self.ctx, "session_uncloned", None) or ()
|
|
1662
|
+
return self._refused_session_read(
|
|
1663
|
+
node, where,
|
|
1664
|
+
f"session.{flag}[...] is not supported here: PineForge keeps a session "
|
|
1665
|
+
"flag's history for the script's top level, a function body and a "
|
|
1666
|
+
"request.security expression that reads it.",
|
|
1667
|
+
owner if owner in uncloned else None,
|
|
1668
|
+
)
|
|
1669
|
+
|
|
1670
|
+
def _refused_session_read(self, node: Subscript, where, message: str,
|
|
1671
|
+
uncloned: str | None = None) -> str:
|
|
1672
|
+
"""A stand-in for a ``session.<flag>[k]`` read PineForge keeps no
|
|
1673
|
+
history for: a name only this read spells, the refusal raised (or, in
|
|
1674
|
+
the uncloned function ``uncloned``, its clones asked for) if the
|
|
1675
|
+
emitted code holds it (``_settle_session_reads``)."""
|
|
1676
|
+
key = (id(node), message)
|
|
1677
|
+
name = self._refused_session_read_names.get(key)
|
|
1678
|
+
if name is None:
|
|
1679
|
+
name = self._allocate_generated_cpp_name(
|
|
1680
|
+
f"_refused_session_read_{len(self._refused_session_reads) + 1}",
|
|
1681
|
+
self._session_names_used)
|
|
1682
|
+
self._refused_session_read_names[key] = name
|
|
1683
|
+
self._refused_session_reads[name] = (node, where, message, uncloned)
|
|
1684
|
+
return name
|
|
1685
|
+
|
|
1686
|
+
def _call_site_var_name(self, node: Identifier, safe: str) -> str:
|
|
1687
|
+
"""``safe`` as this read names it under the active var remap.
|
|
1688
|
+
|
|
1689
|
+
In a callable body the clone remap lists every callable's history and
|
|
1690
|
+
``var`` members, for nested instances to compose, so it only renames a
|
|
1691
|
+
local binding. A read the analyzer resolved to a script-scope binding
|
|
1692
|
+
(also before a same-named local shadows it), a loop binder or a
|
|
1693
|
+
parameter keeps its own name: renaming ``src`` to another callable's
|
|
1694
|
+
``src_cs1``, which nothing writes, read ``na``. Outside callables the
|
|
1695
|
+
remap holds block-scoped renames only, which a top-level branch
|
|
1696
|
+
binding resolved to the global scope still needs.
|
|
1697
|
+
"""
|
|
1698
|
+
if not self._active_var_remap or safe not in self._active_var_remap:
|
|
1699
|
+
return safe
|
|
1700
|
+
if getattr(self, "_active_func_name", None) is not None and (
|
|
1701
|
+
self.ctx.identifier_binding_scopes.get(id(node)) == "global"
|
|
1702
|
+
or node.name in self._current_loop_vars
|
|
1703
|
+
or node.name in self._current_func_param_types
|
|
1704
|
+
):
|
|
1705
|
+
return safe
|
|
1706
|
+
return self._active_var_remap[safe]
|
|
1707
|
+
|
|
1708
|
+
def _history_offset_cpp(self, idx: str, index_node) -> str:
|
|
1709
|
+
"""The ``int`` offset of a history read, safe right after a ``[``.
|
|
1710
|
+
|
|
1711
|
+
A double index narrows through the na-preserving lambda
|
|
1712
|
+
``[&](){ ... }()`` (``_coerce_int_slot``); spelled straight after a
|
|
1713
|
+
subscript's ``[`` it reads ``[[``, which C++ parses as an attribute,
|
|
1714
|
+
so a lambda offset is parenthesized. TradingView truncates a
|
|
1715
|
+
fractional index toward zero (``x[5.5]`` reads ``x[5]``), as the
|
|
1716
|
+
cast does.
|
|
1717
|
+
"""
|
|
1718
|
+
idx_int = self._coerce_int_slot(idx, index_node, "int")
|
|
1719
|
+
if (idx_int == idx
|
|
1720
|
+
and not self._emitted_value_is_double(index_node)):
|
|
1721
|
+
idx_int = pine_index_int_cast(idx)
|
|
1722
|
+
if idx_int.startswith("["):
|
|
1723
|
+
idx_int = f"({idx_int})"
|
|
1724
|
+
return idx_int
|
|
1093
1725
|
|
|
1094
1726
|
def _visit_subscript(self, node: Subscript) -> str:
|
|
1095
1727
|
idx = self._visit_expr(node.index)
|
|
1728
|
+
# Series::operator[] accepts C++ int. A Pine int can be backed by an
|
|
1729
|
+
# int64_t timestamp slot; implicitly narrowing its na sentinel to int
|
|
1730
|
+
# turns it into zero on arm64 and reads the current bar. Preserve the
|
|
1731
|
+
# original value type before narrowing every dynamic series offset.
|
|
1732
|
+
series_idx = (
|
|
1733
|
+
idx if (isinstance(node.index, NumberLiteral)
|
|
1734
|
+
and isinstance(node.index.value, int))
|
|
1735
|
+
else pine_index_int_cast(idx)
|
|
1736
|
+
)
|
|
1096
1737
|
if isinstance(node.object, Identifier):
|
|
1097
1738
|
name = node.object.name
|
|
1739
|
+
# A script variable read through history in a plain UDF body: the
|
|
1740
|
+
# call site's own history of it.
|
|
1741
|
+
if id(node) in self.ctx.func_global_history_nodes:
|
|
1742
|
+
member = self._function_global_history_member(name)
|
|
1743
|
+
if member is not None:
|
|
1744
|
+
return f"{member}[{series_idx}]"
|
|
1098
1745
|
# Function parameters that are series — src[N] → src[N]
|
|
1099
1746
|
if name in self._current_func_series_params:
|
|
1100
|
-
return f"{self._safe_name(name)}[{
|
|
1747
|
+
return f"{self._safe_name(name)}[{series_idx}]"
|
|
1101
1748
|
# Function parameters are scalars — src[0] → src, src[N>0] → src
|
|
1102
1749
|
if name in self._current_func_param_types:
|
|
1103
1750
|
return self._safe_name(name)
|
|
1104
1751
|
if name in BAR_FIELDS or name in BAR_SERIES_PUSH:
|
|
1105
1752
|
# Index matches Pine: [0] current bar, [k] k bars ago (runtime Series deque).
|
|
1106
|
-
return f"_s_{name}[{
|
|
1107
|
-
safe = self._safe_name(name)
|
|
1753
|
+
return f"_s_{name}[{series_idx}]"
|
|
1108
1754
|
# Apply per-call-site / exact block-member remap before deciding
|
|
1109
1755
|
# whether the current lexical binding is a Series.
|
|
1110
|
-
|
|
1111
|
-
safe = self._active_var_remap[safe]
|
|
1756
|
+
safe = self._call_site_var_name(node.object, self._safe_name(name))
|
|
1112
1757
|
if self._binding_is_series(name, safe):
|
|
1113
1758
|
# Same Pine [k] semantics as Series in runtime/series.hpp
|
|
1114
|
-
return f"{safe}[{
|
|
1759
|
+
return f"{safe}[{series_idx}]"
|
|
1115
1760
|
spec = self._collection_spec_for_name(name)
|
|
1116
1761
|
if spec is not None and spec.kind in ("array", "map"):
|
|
1762
|
+
self._codegen_warning(
|
|
1763
|
+
node,
|
|
1764
|
+
f"{spec.kind} history indexing uses the current collection "
|
|
1765
|
+
"element in PineForge; the engine does not retain per-bar "
|
|
1766
|
+
"collection IDs, so this result can differ from TradingView "
|
|
1767
|
+
"and a missing index is not represented faithfully.",
|
|
1768
|
+
)
|
|
1117
1769
|
return f"{self._collection_receiver_expr(name)}[{idx}]"
|
|
1118
1770
|
# Handle strategy.* history access (e.g., strategy.position_size[1])
|
|
1119
1771
|
if isinstance(node.object, MemberAccess):
|
|
@@ -1125,7 +1777,9 @@ class ExprVisitor:
|
|
|
1125
1777
|
series_name = f"_strat_{member}"
|
|
1126
1778
|
if series_name not in self._strategy_series_vars:
|
|
1127
1779
|
self._strategy_series_vars.add(series_name)
|
|
1128
|
-
return f"{series_name}[{
|
|
1780
|
+
return f"{series_name}[{series_idx}]"
|
|
1781
|
+
if self._is_session_flag(node.object):
|
|
1782
|
+
return self._visit_session_history(node, series_idx)
|
|
1129
1783
|
# History reference applied directly to an inline call result, e.g.
|
|
1130
1784
|
# ``ta.highest(high, 10)[1]`` or ``f()[2]``. In Pine the call yields a
|
|
1131
1785
|
# series, so ``[k]`` reads its value k bars ago — but the call lowers to
|
|
@@ -1144,7 +1798,8 @@ class ExprVisitor:
|
|
|
1144
1798
|
# previous chart bar in every run mode (``_lazy_edge_ta_hoist_plan``).
|
|
1145
1799
|
hoisted_member = self._hoisted_hist_reads.get(id(node))
|
|
1146
1800
|
if hoisted_member is not None:
|
|
1147
|
-
|
|
1801
|
+
idx_int = self._history_offset_cpp(idx, node.index)
|
|
1802
|
+
return f"{hoisted_member}[{idx_int}]"
|
|
1148
1803
|
inner = self._visit_expr(node.object)
|
|
1149
1804
|
cpp_t = self._infer_type(node.object)
|
|
1150
1805
|
if cpp_t not in ("double", "int", "int64_t", "bool"):
|
|
@@ -1175,6 +1830,7 @@ class ExprVisitor:
|
|
|
1175
1830
|
# established call-local history fallback below.
|
|
1176
1831
|
ta_mem = self._ta_member_name(ta_site)
|
|
1177
1832
|
precalc = f"_precalc_{ta_mem}"
|
|
1833
|
+
idx_int = self._history_offset_cpp(idx, node.index)
|
|
1178
1834
|
return (
|
|
1179
1835
|
f"([&]() -> {cpp_t} {{ "
|
|
1180
1836
|
f"if (_use_precalc) {{ "
|
|
@@ -1196,15 +1852,39 @@ class ExprVisitor:
|
|
|
1196
1852
|
f"{cpp_t} _hv = ({inner}); "
|
|
1197
1853
|
f"if (history_advances_new_bar()) {member}.push(_hv); "
|
|
1198
1854
|
f"else {member}.update(_hv); "
|
|
1199
|
-
f"return {member}[
|
|
1855
|
+
f"return {member}[{idx_int}]; }}())"
|
|
1200
1856
|
)
|
|
1857
|
+
idx_int = self._history_offset_cpp(idx, node.index)
|
|
1201
1858
|
return (
|
|
1202
1859
|
f"([&]() -> {cpp_t} {{ "
|
|
1203
1860
|
f"{cpp_t} _hv = ({inner}); "
|
|
1204
1861
|
f"if (history_advances_new_bar()) {member}.push(_hv); "
|
|
1205
1862
|
f"else {member}.update(_hv); "
|
|
1206
|
-
f"return {member}[
|
|
1863
|
+
f"return {member}[{idx_int}]; }}())"
|
|
1864
|
+
)
|
|
1865
|
+
# History on an operator expression (``(a > b)[1]``) reads the value
|
|
1866
|
+
# the expression had k bars ago: its prepass-registered synthetic
|
|
1867
|
+
# Series (``_is_compound_history_object``) is pushed once per
|
|
1868
|
+
# evaluation, like the inline call results above.
|
|
1869
|
+
compound_member = (
|
|
1870
|
+
self._inline_history_member_by_key.get(
|
|
1871
|
+
("hist_call", id(node), self._current_instance_name)
|
|
1207
1872
|
)
|
|
1873
|
+
if isinstance(node.object, (BinOp, UnaryOp, Ternary))
|
|
1874
|
+
else None
|
|
1875
|
+
)
|
|
1876
|
+
if compound_member is not None:
|
|
1877
|
+
cpp_t = self._infer_type(node.object)
|
|
1878
|
+
if cpp_t in ("double", "int", "int64_t", "bool"):
|
|
1879
|
+
inner = self._visit_expr(node.object)
|
|
1880
|
+
idx_int = self._history_offset_cpp(idx, node.index)
|
|
1881
|
+
return (
|
|
1882
|
+
f"([&]() -> {cpp_t} {{ "
|
|
1883
|
+
f"{cpp_t} _hv = ({inner}); "
|
|
1884
|
+
f"if (history_advances_new_bar()) {compound_member}.push(_hv); "
|
|
1885
|
+
f"else {compound_member}.update(_hv); "
|
|
1886
|
+
f"return {compound_member}[{idx_int}]; }}())"
|
|
1887
|
+
)
|
|
1208
1888
|
obj = self._visit_expr(node.object)
|
|
1209
1889
|
# If subscripting a non-series variable (e.g., function parameter),
|
|
1210
1890
|
# src[0] → src (current value), src[N>0] → src (can't access history)
|
|
@@ -1214,4 +1894,5 @@ class ExprVisitor:
|
|
|
1214
1894
|
and name not in self.ctx.series_vars
|
|
1215
1895
|
and name not in self._var_names):
|
|
1216
1896
|
return obj
|
|
1217
|
-
|
|
1897
|
+
idx_int = self._history_offset_cpp(idx, node.index)
|
|
1898
|
+
return f"{obj}[{idx_int}]"
|