@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
|
@@ -73,6 +73,7 @@ classes from ``..ast_nodes``.
|
|
|
73
73
|
|
|
74
74
|
from __future__ import annotations
|
|
75
75
|
|
|
76
|
+
from ..errors import Phase
|
|
76
77
|
from ..ast_nodes import (
|
|
77
78
|
ASTNode,
|
|
78
79
|
Assignment,
|
|
@@ -99,6 +100,7 @@ from ..ast_nodes import (
|
|
|
99
100
|
WhileStmt,
|
|
100
101
|
)
|
|
101
102
|
from ..symbols import PineType, TypeSpec, method_receiver_type_name
|
|
103
|
+
from ..external_requests import UNPINNED_ANNOTATION
|
|
102
104
|
from .tables import (
|
|
103
105
|
ARRAY_NEW_CTORS,
|
|
104
106
|
DRAWING_TYPE_TO_CPP,
|
|
@@ -123,6 +125,10 @@ class StmtVisitor:
|
|
|
123
125
|
# ------------------------------------------------------------------
|
|
124
126
|
|
|
125
127
|
def _visit_stmt(self, node: ASTNode, lines: list[str], indent: int) -> None:
|
|
128
|
+
if self._budget is not None:
|
|
129
|
+
self._budget_visit_count += 1
|
|
130
|
+
if self._budget_visit_count % 128 == 0:
|
|
131
|
+
self._budget.check(node.loc, Phase.CODEGEN)
|
|
126
132
|
pad = " " * indent
|
|
127
133
|
|
|
128
134
|
if isinstance(node, StrategyDecl):
|
|
@@ -346,35 +352,44 @@ class StmtVisitor:
|
|
|
346
352
|
and node.expr.args):
|
|
347
353
|
risk_func = c.member
|
|
348
354
|
_RISK_MEMBER_MAP = {
|
|
349
|
-
"max_intraday_filled_orders": (
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
"
|
|
353
|
-
"
|
|
355
|
+
"max_intraday_filled_orders": (
|
|
356
|
+
"set_pine_risk_max_intraday_filled_orders", "int"
|
|
357
|
+
),
|
|
358
|
+
"max_drawdown": ("set_pine_risk_max_drawdown", "double"),
|
|
359
|
+
"max_intraday_loss": ("set_pine_risk_max_intraday_loss", "double"),
|
|
360
|
+
"max_position_size": ("set_pine_risk_max_position_size", "double"),
|
|
361
|
+
"max_cons_loss_days": ("set_pine_risk_max_cons_loss_days", "int"),
|
|
354
362
|
}
|
|
355
363
|
if risk_func == "allow_entry_in":
|
|
356
364
|
val = self._visit_expr(node.expr.args[0])
|
|
357
365
|
if val == "1":
|
|
358
|
-
|
|
366
|
+
direction = "1"
|
|
359
367
|
elif val == "-1":
|
|
360
|
-
|
|
368
|
+
direction = "-1"
|
|
361
369
|
else:
|
|
362
|
-
|
|
370
|
+
direction = "0"
|
|
371
|
+
lines.append(f"{pad}set_pine_risk_direction({direction});")
|
|
363
372
|
return
|
|
364
373
|
if risk_func in _RISK_MEMBER_MAP:
|
|
365
|
-
|
|
374
|
+
setter, cast_type = _RISK_MEMBER_MAP[risk_func]
|
|
366
375
|
val = self._visit_expr(node.expr.args[0])
|
|
367
|
-
|
|
368
|
-
#
|
|
376
|
+
# The percent flag travels with the matching setter so
|
|
377
|
+
# every risk update atomically replaces both fields.
|
|
369
378
|
if risk_func in ("max_drawdown", "max_intraday_loss") and len(node.expr.args) >= 2:
|
|
370
379
|
arg2 = node.expr.args[1]
|
|
371
380
|
is_pct = (isinstance(arg2, MemberAccess)
|
|
372
381
|
and isinstance(arg2.object, Identifier)
|
|
373
382
|
and arg2.object.name == "strategy"
|
|
374
383
|
and arg2.member == "percent_of_equity")
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
384
|
+
else:
|
|
385
|
+
is_pct = False
|
|
386
|
+
if risk_func in ("max_drawdown", "max_intraday_loss"):
|
|
387
|
+
lines.append(
|
|
388
|
+
f"{pad}{setter}(({cast_type})({val}), "
|
|
389
|
+
f"{'true' if is_pct else 'false'});"
|
|
390
|
+
)
|
|
391
|
+
else:
|
|
392
|
+
lines.append(f"{pad}{setter}(({cast_type})({val}));")
|
|
378
393
|
return
|
|
379
394
|
if self._is_skip_expr(node.expr):
|
|
380
395
|
return
|
|
@@ -530,6 +545,37 @@ class StmtVisitor:
|
|
|
530
545
|
lines.append(f"{pad} {flag_expr} = true;")
|
|
531
546
|
lines.append(f"{pad}}}")
|
|
532
547
|
return
|
|
548
|
+
if isinstance(node.value, (IfStmt, SwitchStmt)):
|
|
549
|
+
# Any other ``var`` whose initializer is an if/switch
|
|
550
|
+
# expression: the selection, once, at first reach. It
|
|
551
|
+
# rendered as ``/* unknown */``, which did not compile
|
|
552
|
+
# (``var string dashPos = switch dashPosInput``). A
|
|
553
|
+
# history-referenced one replaces its current slot.
|
|
554
|
+
selection_cpp_type = (
|
|
555
|
+
target_cpp_type or self._int_slot_cpp_type(member_name)
|
|
556
|
+
)
|
|
557
|
+
indent = len(pad) // 4 + 1
|
|
558
|
+
lines.append(f"{pad}if (!{flag_expr}) {{")
|
|
559
|
+
if info.get("is_series"):
|
|
560
|
+
# The member's own element type (string, drawing
|
|
561
|
+
# or number), from its current slot: the carry
|
|
562
|
+
# has pushed the var's na there before its first
|
|
563
|
+
# reach, which an arm no case selects keeps.
|
|
564
|
+
selected = f"_pf_selection_{flag}"
|
|
565
|
+
lines.append(f"{pad} auto {selected} = {target_expr}[0];")
|
|
566
|
+
self._visit_if_switch_expr(
|
|
567
|
+
node.value, selected, lines, indent,
|
|
568
|
+
target_cpp_type=selection_cpp_type,
|
|
569
|
+
)
|
|
570
|
+
lines.append(f"{pad} {target_expr}.update({selected});")
|
|
571
|
+
else:
|
|
572
|
+
self._visit_if_switch_expr(
|
|
573
|
+
node.value, target_expr, lines, indent,
|
|
574
|
+
target_cpp_type=selection_cpp_type,
|
|
575
|
+
)
|
|
576
|
+
lines.append(f"{pad} {flag_expr} = true;")
|
|
577
|
+
lines.append(f"{pad}}}")
|
|
578
|
+
return
|
|
533
579
|
if target_cpp_type is not None:
|
|
534
580
|
init_cpp = self._visit_rhs_value(
|
|
535
581
|
node.value,
|
|
@@ -545,6 +591,10 @@ class StmtVisitor:
|
|
|
545
591
|
init_cpp = self._typed_na_init(
|
|
546
592
|
init_cpp, member_name, info["ptype"]
|
|
547
593
|
)
|
|
594
|
+
init_cpp = self._coerce_int_slot(
|
|
595
|
+
init_cpp, node.value,
|
|
596
|
+
self._int_slot_cpp_type(member_name),
|
|
597
|
+
)
|
|
548
598
|
lines.append(f"{pad}if (!{flag_expr}) {{")
|
|
549
599
|
# A history-referenced persistent primitive is a Series<T> too,
|
|
550
600
|
# not just a drawing handle. The per-bar carry has already
|
|
@@ -660,10 +710,17 @@ class StmtVisitor:
|
|
|
660
710
|
)
|
|
661
711
|
elem_spec = spec.element or elem_spec
|
|
662
712
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
713
|
+
if len(node.value.args) > 2:
|
|
714
|
+
self._warn_narrow_int_element(
|
|
715
|
+
node.value, spec, node.value.args[2])
|
|
663
716
|
if len(node.value.args) >= 2:
|
|
664
717
|
r = self._visit_expr(node.value.args[0])
|
|
665
718
|
c = self._visit_expr(node.value.args[1])
|
|
666
|
-
v =
|
|
719
|
+
v = (
|
|
720
|
+
self._array_init_value_expr(elem_spec, node.value.args[2])
|
|
721
|
+
if len(node.value.args) > 2
|
|
722
|
+
else self._default_for_spec(elem_spec)
|
|
723
|
+
)
|
|
667
724
|
init = f"{cpp_type}::new_({r}, {c}, {v})"
|
|
668
725
|
else:
|
|
669
726
|
init = f"{cpp_type}::new_(0, 0, {self._default_for_spec(elem_spec)})"
|
|
@@ -738,12 +795,24 @@ class StmtVisitor:
|
|
|
738
795
|
site = self._get_ta_site(node.value)
|
|
739
796
|
if site is not None:
|
|
740
797
|
compute_args = self._ta_compute_args_for_site(site)
|
|
741
|
-
ret_type =
|
|
798
|
+
ret_type = (
|
|
799
|
+
self._type_for_decl(node)
|
|
800
|
+
if node.type_hint and not is_global_member
|
|
801
|
+
else "bool" if self._ta_name_from_site(site) in TA_RETURNS_BOOL
|
|
802
|
+
else "double"
|
|
803
|
+
)
|
|
742
804
|
ta_name = self._ta_member_name(site)
|
|
743
805
|
ta_expr = (
|
|
744
806
|
f"(history_advances_new_bar() ? {ta_name}.compute({compute_args}) "
|
|
745
807
|
f": {ta_name}.recompute({compute_args}))"
|
|
746
808
|
)
|
|
809
|
+
ta_expr = self._coerce_int_slot(
|
|
810
|
+
ta_expr,
|
|
811
|
+
node.value,
|
|
812
|
+
self._int_slot_cpp_type(
|
|
813
|
+
node.name, None if is_global_member else ret_type
|
|
814
|
+
),
|
|
815
|
+
)
|
|
747
816
|
if declaration_is_series:
|
|
748
817
|
self._emit_history_series_write(lines, pad, safe, ta_expr)
|
|
749
818
|
elif is_global_member:
|
|
@@ -764,6 +833,10 @@ class StmtVisitor:
|
|
|
764
833
|
else None
|
|
765
834
|
),
|
|
766
835
|
)
|
|
836
|
+
cpp_val = self._coerce_int_slot(
|
|
837
|
+
cpp_val, node.value,
|
|
838
|
+
self._int_slot_cpp_type(None, self._series_type_for(node.name)),
|
|
839
|
+
)
|
|
767
840
|
self._emit_history_series_write(lines, pad, safe, cpp_val)
|
|
768
841
|
return
|
|
769
842
|
|
|
@@ -791,17 +864,25 @@ class StmtVisitor:
|
|
|
791
864
|
node.name,
|
|
792
865
|
cpp_type,
|
|
793
866
|
)
|
|
867
|
+
if selection_cpp_type is None:
|
|
868
|
+
selection_cpp_type = self._int_slot_cpp_type(
|
|
869
|
+
node.name, cpp_type,
|
|
870
|
+
)
|
|
794
871
|
if not is_global_member:
|
|
795
872
|
default = self._default_for_type(cpp_type)
|
|
796
873
|
lines.append(f"{pad}{cpp_type} {safe} = {default};")
|
|
797
874
|
remember_local_type(cpp_type)
|
|
798
875
|
indent = len(pad) // 4
|
|
799
|
-
self.
|
|
876
|
+
self._visit_selection_value(
|
|
800
877
|
node.value,
|
|
801
878
|
safe,
|
|
802
879
|
lines,
|
|
803
880
|
indent,
|
|
804
881
|
target_cpp_type=selection_cpp_type,
|
|
882
|
+
slot_cpp_type=(
|
|
883
|
+
cpp_type if cpp_type is not None
|
|
884
|
+
else self._infer_type(Identifier(name=node.name))
|
|
885
|
+
),
|
|
805
886
|
)
|
|
806
887
|
return
|
|
807
888
|
|
|
@@ -851,6 +932,12 @@ class StmtVisitor:
|
|
|
851
932
|
node.name,
|
|
852
933
|
target_cpp_type=target_cpp_type,
|
|
853
934
|
)
|
|
935
|
+
# ``cpp_type`` is None for a hoisted global; the slot resolver then
|
|
936
|
+
# reads the member declaration instead.
|
|
937
|
+
cpp_val = self._coerce_int_slot(
|
|
938
|
+
cpp_val, node.value,
|
|
939
|
+
self._int_slot_cpp_type(node.name, cpp_type),
|
|
940
|
+
)
|
|
854
941
|
if is_global_member:
|
|
855
942
|
lines.append(f"{pad}{safe} = {cpp_val};")
|
|
856
943
|
else:
|
|
@@ -983,13 +1070,16 @@ class StmtVisitor:
|
|
|
983
1070
|
target_name,
|
|
984
1071
|
None,
|
|
985
1072
|
)
|
|
1073
|
+
if selection_cpp_type is None:
|
|
1074
|
+
selection_cpp_type = self._int_slot_cpp_type(target_name)
|
|
986
1075
|
indent = len(pad) // 4
|
|
987
|
-
self.
|
|
1076
|
+
self._visit_selection_value(
|
|
988
1077
|
node.value,
|
|
989
1078
|
safe,
|
|
990
1079
|
lines,
|
|
991
1080
|
indent,
|
|
992
1081
|
target_cpp_type=selection_cpp_type,
|
|
1082
|
+
slot_cpp_type=self._infer_type(node.target),
|
|
993
1083
|
)
|
|
994
1084
|
return
|
|
995
1085
|
|
|
@@ -1022,13 +1112,28 @@ class StmtVisitor:
|
|
|
1022
1112
|
val_cpp = self._visit_rhs_value(
|
|
1023
1113
|
node.value, target_cpp_type=target_cpp_type
|
|
1024
1114
|
)
|
|
1025
|
-
|
|
1115
|
+
field_int = self._udt_field_int_cpp_type(node.target)
|
|
1116
|
+
array_field = self._udt_array_field_target(node.target)
|
|
1117
|
+
if node.op == ":=" and array_field is not None:
|
|
1118
|
+
# The field rebinds to the array: an alias of a stable
|
|
1119
|
+
# script var array, else the value moved or copied in.
|
|
1120
|
+
lines.append(
|
|
1121
|
+
f"{pad}{target_cpp} = "
|
|
1122
|
+
f"{self._udt_array_field_value(array_field, node.value, val_cpp)};")
|
|
1123
|
+
elif node.op == ":=":
|
|
1124
|
+
val_cpp = self._coerce_int_slot(val_cpp, node.value, field_int)
|
|
1026
1125
|
lines.append(f"{pad}{target_cpp} = {val_cpp};")
|
|
1027
1126
|
else:
|
|
1028
1127
|
rhs = self._compound_assign_rhs(target_cpp, node.op, val_cpp)
|
|
1029
1128
|
if rhs is not None:
|
|
1129
|
+
rhs = self._coerce_int_slot(
|
|
1130
|
+
rhs, node.value, field_int, value_is_double=True,
|
|
1131
|
+
)
|
|
1030
1132
|
lines.append(f"{pad}{target_cpp} = {rhs};")
|
|
1031
1133
|
else:
|
|
1134
|
+
val_cpp = self._coerce_int_slot(
|
|
1135
|
+
val_cpp, node.value, field_int,
|
|
1136
|
+
)
|
|
1032
1137
|
lines.append(f"{pad}{target_cpp} {node.op} {val_cpp};")
|
|
1033
1138
|
return
|
|
1034
1139
|
|
|
@@ -1046,17 +1151,27 @@ class StmtVisitor:
|
|
|
1046
1151
|
None,
|
|
1047
1152
|
),
|
|
1048
1153
|
)
|
|
1154
|
+
elem_int = self._int_slot_cpp_type(
|
|
1155
|
+
None, self._series_type_for(target_name),
|
|
1156
|
+
)
|
|
1049
1157
|
if node.op == ":=":
|
|
1158
|
+
val_cpp = self._coerce_int_slot(val_cpp, node.value, elem_int)
|
|
1050
1159
|
lines.append(f"{pad}{safe}.update({val_cpp});")
|
|
1051
1160
|
else:
|
|
1052
1161
|
rhs = self._compound_assign_rhs(f"{safe}[0]", node.op, val_cpp)
|
|
1053
1162
|
if rhs is not None:
|
|
1054
1163
|
# x /= y → x.update((double)x[0] / (double)y); x %= y → fmod
|
|
1164
|
+
rhs = self._coerce_int_slot(
|
|
1165
|
+
rhs, node.value, elem_int, value_is_double=True,
|
|
1166
|
+
)
|
|
1055
1167
|
lines.append(f"{pad}{safe}.update({rhs});")
|
|
1056
1168
|
else:
|
|
1057
1169
|
# Compound assignment: x += y → x.update(x[0] + y)
|
|
1058
1170
|
op_char = node.op[0] # e.g., "+" from "+="
|
|
1059
|
-
|
|
1171
|
+
combined = self._coerce_int_slot(
|
|
1172
|
+
f"{safe}[0] {op_char} {val_cpp}", node.value, elem_int,
|
|
1173
|
+
)
|
|
1174
|
+
lines.append(f"{pad}{safe}.update({combined});")
|
|
1060
1175
|
elif target_name in self._var_names:
|
|
1061
1176
|
# A bare-``na`` reassignment must adopt the target's declared scalar
|
|
1062
1177
|
# type (``x := na`` -> ``na<int>()`` not ``na<double>()``); otherwise
|
|
@@ -1069,14 +1184,30 @@ class StmtVisitor:
|
|
|
1069
1184
|
if tct is None and self._is_na_expr(node.value):
|
|
1070
1185
|
tct = self._na_reassign_cpp_type(target_name)
|
|
1071
1186
|
val_cpp = self._visit_rhs_value(node.value, target_name, target_cpp_type=tct)
|
|
1187
|
+
int_slot = self._int_slot_cpp_type(target_name)
|
|
1072
1188
|
if node.op == ":=":
|
|
1189
|
+
val_cpp = self._coerce_int_slot(val_cpp, node.value, int_slot)
|
|
1073
1190
|
lines.append(f"{pad}{safe} = {val_cpp};")
|
|
1074
1191
|
else:
|
|
1075
1192
|
rhs = self._compound_assign_rhs(safe, node.op, val_cpp)
|
|
1076
1193
|
if rhs is not None:
|
|
1194
|
+
# ``/=`` and ``%=`` lower to an always-double form, so an
|
|
1195
|
+
# int target narrows whatever the operands were.
|
|
1196
|
+
rhs = self._coerce_int_slot(
|
|
1197
|
+
rhs, node.value, int_slot, value_is_double=True,
|
|
1198
|
+
)
|
|
1077
1199
|
lines.append(f"{pad}{safe} = {rhs};")
|
|
1078
1200
|
else:
|
|
1079
|
-
|
|
1201
|
+
# Narrow the WHOLE compound expression, not the operand:
|
|
1202
|
+
# ``x -= 0.5`` on an int slot truncated (x - 0.5), and
|
|
1203
|
+
# truncating the operand first would round differently.
|
|
1204
|
+
combined = self._coerce_int_slot(
|
|
1205
|
+
f"{safe} {node.op[0]} {val_cpp}", node.value, int_slot,
|
|
1206
|
+
)
|
|
1207
|
+
if combined != f"{safe} {node.op[0]} {val_cpp}":
|
|
1208
|
+
lines.append(f"{pad}{safe} = {combined};")
|
|
1209
|
+
else:
|
|
1210
|
+
lines.append(f"{pad}{safe} {node.op} {val_cpp};")
|
|
1080
1211
|
else:
|
|
1081
1212
|
tct = self._nullable_collection_target_cpp_type(name=target_name)
|
|
1082
1213
|
if tct is None:
|
|
@@ -1084,14 +1215,30 @@ class StmtVisitor:
|
|
|
1084
1215
|
if tct is None and self._is_na_expr(node.value):
|
|
1085
1216
|
tct = self._na_reassign_cpp_type(target_name)
|
|
1086
1217
|
val_cpp = self._visit_rhs_value(node.value, target_name, target_cpp_type=tct)
|
|
1218
|
+
int_slot = self._int_slot_cpp_type(target_name)
|
|
1087
1219
|
if node.op == ":=":
|
|
1220
|
+
val_cpp = self._coerce_int_slot(val_cpp, node.value, int_slot)
|
|
1088
1221
|
lines.append(f"{pad}{safe} = {val_cpp};")
|
|
1089
1222
|
else:
|
|
1090
1223
|
rhs = self._compound_assign_rhs(safe, node.op, val_cpp)
|
|
1091
1224
|
if rhs is not None:
|
|
1225
|
+
# ``/=`` and ``%=`` lower to an always-double form, so an
|
|
1226
|
+
# int target narrows whatever the operands were.
|
|
1227
|
+
rhs = self._coerce_int_slot(
|
|
1228
|
+
rhs, node.value, int_slot, value_is_double=True,
|
|
1229
|
+
)
|
|
1092
1230
|
lines.append(f"{pad}{safe} = {rhs};")
|
|
1093
1231
|
else:
|
|
1094
|
-
|
|
1232
|
+
# Narrow the WHOLE compound expression, not the operand:
|
|
1233
|
+
# ``x -= 0.5`` on an int slot truncated (x - 0.5), and
|
|
1234
|
+
# truncating the operand first would round differently.
|
|
1235
|
+
combined = self._coerce_int_slot(
|
|
1236
|
+
f"{safe} {node.op[0]} {val_cpp}", node.value, int_slot,
|
|
1237
|
+
)
|
|
1238
|
+
if combined != f"{safe} {node.op[0]} {val_cpp}":
|
|
1239
|
+
lines.append(f"{pad}{safe} = {combined};")
|
|
1240
|
+
else:
|
|
1241
|
+
lines.append(f"{pad}{safe} {node.op} {val_cpp};")
|
|
1095
1242
|
|
|
1096
1243
|
def _visit_tuple_assign(self, node: TupleAssign, lines: list[str], pad: str) -> None:
|
|
1097
1244
|
is_top_level = any(id(node) == id(stmt) for stmt in self.ctx.ast.body)
|
|
@@ -1152,6 +1299,14 @@ class StmtVisitor:
|
|
|
1152
1299
|
and len(tuple_types) == tuple_size
|
|
1153
1300
|
and all(item == PineType.BOOL for item in tuple_types)
|
|
1154
1301
|
)
|
|
1302
|
+
# A helper tuple mixing bool, string and numeric
|
|
1303
|
+
# elements (the analyzer admits nothing else).
|
|
1304
|
+
mixed_helper_tuple = (
|
|
1305
|
+
not isinstance(expr_node, TupleLiteral)
|
|
1306
|
+
and info.get("returns_tuple", False)
|
|
1307
|
+
and tuple_size >= 2
|
|
1308
|
+
and len(tuple_types) == tuple_size
|
|
1309
|
+
)
|
|
1155
1310
|
exact_direct_tuple = (
|
|
1156
1311
|
isinstance(expr_node, TupleLiteral)
|
|
1157
1312
|
and tuple_size >= 2
|
|
@@ -1177,6 +1332,7 @@ class StmtVisitor:
|
|
|
1177
1332
|
if (
|
|
1178
1333
|
numeric_tuple
|
|
1179
1334
|
or bool_tuple
|
|
1335
|
+
or mixed_helper_tuple
|
|
1180
1336
|
or exact_direct_tuple
|
|
1181
1337
|
or known_ta_tuple
|
|
1182
1338
|
):
|
|
@@ -1193,6 +1349,10 @@ class StmtVisitor:
|
|
|
1193
1349
|
the tuple once whenever either case applies; ordinary lexical scalar
|
|
1194
1350
|
elements remain locals, Series elements advance their remapped
|
|
1195
1351
|
members, and top-level scalars assign their class storage.
|
|
1352
|
+
Pine's ``_`` binds nothing, but a structured binding declares
|
|
1353
|
+
every name: C++17 has no placeholder, and GCC rejects a second
|
|
1354
|
+
``_`` in one scope (``[a, _, _] = f()``, or two tuples of one
|
|
1355
|
+
block) as a redeclaration. Each ``_`` gets a name of its own.
|
|
1196
1356
|
"""
|
|
1197
1357
|
series_names = {
|
|
1198
1358
|
name
|
|
@@ -1201,7 +1361,11 @@ class StmtVisitor:
|
|
|
1201
1361
|
and self._decl_binding_is_series(id(node), name)
|
|
1202
1362
|
}
|
|
1203
1363
|
if not series_names and not global_targets.intersection(node.names):
|
|
1204
|
-
binding_names = ", ".join(
|
|
1364
|
+
binding_names = ", ".join(
|
|
1365
|
+
self._tuple_placeholder_name() if name == "_"
|
|
1366
|
+
else self._safe_name(name)
|
|
1367
|
+
for name in node.names
|
|
1368
|
+
)
|
|
1205
1369
|
lines.append(f"{pad}auto [{binding_names}] = {call_expr};")
|
|
1206
1370
|
return
|
|
1207
1371
|
|
|
@@ -1270,14 +1434,38 @@ class StmtVisitor:
|
|
|
1270
1434
|
f"{pad}{self._safe_name(name)} = {field_expr};"
|
|
1271
1435
|
)
|
|
1272
1436
|
else:
|
|
1273
|
-
lines.append(
|
|
1437
|
+
lines.append(
|
|
1438
|
+
f"{pad}double {self._safe_name(name)} = {field_expr};"
|
|
1439
|
+
)
|
|
1440
|
+
return
|
|
1441
|
+
|
|
1442
|
+
# ``[a, b] = switch x ...`` / ``= if c ...``: every arm yields a tuple
|
|
1443
|
+
# (the analyzer's ``_selection_tuple_shape``). Materialize the selected
|
|
1444
|
+
# tuple, na in every position when no arm runs, then destructure it.
|
|
1445
|
+
if isinstance(node.value, (IfStmt, SwitchStmt)):
|
|
1446
|
+
types = self._infer_selection_tuple_types(
|
|
1447
|
+
node.value, len(node.names)
|
|
1448
|
+
)
|
|
1449
|
+
tuple_t = f"std::tuple<{', '.join(types)}>"
|
|
1450
|
+
temp = f"_tuple_result_{self._tuple_assign_counter}"
|
|
1451
|
+
self._tuple_assign_counter += 1
|
|
1452
|
+
lines.append(f"{pad}{tuple_t} {temp} = {self._tuple_default_expr(types)};")
|
|
1453
|
+
self._visit_if_switch_expr(
|
|
1454
|
+
node.value, temp, lines, len(pad) // 4, target_cpp_type=tuple_t
|
|
1455
|
+
)
|
|
1456
|
+
emit_call_tuple(temp)
|
|
1274
1457
|
return
|
|
1275
1458
|
|
|
1276
1459
|
# User-defined function returning a tuple: use C++17 structured bindings
|
|
1277
1460
|
if isinstance(node.value, FuncCall):
|
|
1278
1461
|
func_name, namespace = self._resolve_callee(node.value.callee)
|
|
1279
1462
|
if namespace == "request" and func_name == "security":
|
|
1280
|
-
|
|
1463
|
+
# A request whose names are reassigned is read where it is
|
|
1464
|
+
# evaluated (external_requests): its data-missing stop.
|
|
1465
|
+
if UNPINNED_ANNOTATION in (node.value.annotations or {}):
|
|
1466
|
+
call_expr = self._unpinned_read(node.value)
|
|
1467
|
+
else:
|
|
1468
|
+
call_expr = self._visit_func_call(node.value)
|
|
1281
1469
|
emit_call_tuple(call_expr)
|
|
1282
1470
|
return
|
|
1283
1471
|
if func_name and namespace is None and func_name in self._func_names:
|
|
@@ -1311,6 +1499,13 @@ class StmtVisitor:
|
|
|
1311
1499
|
|
|
1312
1500
|
lines.append(f"{pad}/* unsupported tuple assignment */")
|
|
1313
1501
|
|
|
1502
|
+
def _tuple_placeholder_name(self) -> str:
|
|
1503
|
+
"""A fresh C++ name for one ``_`` of a tuple declaration's structured
|
|
1504
|
+
binding, which must name every element."""
|
|
1505
|
+
index = getattr(self, "_tuple_placeholder_counter", 0)
|
|
1506
|
+
self._tuple_placeholder_counter = index + 1
|
|
1507
|
+
return f"_tuple_unused_{index}"
|
|
1508
|
+
|
|
1314
1509
|
def _tuple_binding_cpp_types(self, node: TupleAssign) -> list[str]:
|
|
1315
1510
|
"""Exact supported tuple element types for later lexical operations."""
|
|
1316
1511
|
count = len(node.names)
|
|
@@ -1477,7 +1672,9 @@ class StmtVisitor:
|
|
|
1477
1672
|
def _visit_if_body(self, node: IfStmt, lines: list[str], indent: int) -> None:
|
|
1478
1673
|
pad = " " * indent
|
|
1479
1674
|
|
|
1480
|
-
cond = self.
|
|
1675
|
+
cond = self._coerce_bool_expr(
|
|
1676
|
+
self._visit_expr(node.condition), node.condition
|
|
1677
|
+
)
|
|
1481
1678
|
lines.append(f"{pad}if ({cond}) {{")
|
|
1482
1679
|
self._visit_block_statements(node.body, lines, indent + 1)
|
|
1483
1680
|
lines.append(f"{pad}}}")
|
|
@@ -1492,7 +1689,8 @@ class StmtVisitor:
|
|
|
1492
1689
|
)
|
|
1493
1690
|
lines.append(f"{pad}}}")
|
|
1494
1691
|
|
|
1495
|
-
def _visit_for(self, node: ForStmt, lines: list[str], indent: int
|
|
1692
|
+
def _visit_for(self, node: ForStmt, lines: list[str], indent: int,
|
|
1693
|
+
value_target: tuple[str, str | None] | None = None) -> None:
|
|
1496
1694
|
pad = " " * indent
|
|
1497
1695
|
start = self._visit_expr(node.start)
|
|
1498
1696
|
end = self._visit_expr(node.end)
|
|
@@ -1518,35 +1716,69 @@ class StmtVisitor:
|
|
|
1518
1716
|
)
|
|
1519
1717
|
)
|
|
1520
1718
|
end_eval = f"_for_end_eval_{fid}" if end_mentions_binder else None
|
|
1719
|
+
# ``from``/``to``/``by`` land in ``int`` slots. A double-valued bound
|
|
1720
|
+
# (an array size, an na-able expression, ...) narrows there, which is
|
|
1721
|
+
# undefined for na — so spell the conversion out. A bound that IS na
|
|
1722
|
+
# then reads as na<int>() (INT_MIN), and the loop must run zero times
|
|
1723
|
+
# rather than descend two billion steps toward it, so the guarded form
|
|
1724
|
+
# also tests the bounds. Loops whose bounds were already integral emit
|
|
1725
|
+
# exactly as before.
|
|
1726
|
+
start_cpp = self._coerce_int_slot(f"({start})", node.start, "int")
|
|
1727
|
+
end_cpp = self._coerce_int_slot(f"({end})", node.end, "int")
|
|
1728
|
+
step_cpp = self._coerce_int_slot(f"({step})", node.step, "int")
|
|
1729
|
+
na_capable = (
|
|
1730
|
+
start_cpp != f"({start})"
|
|
1731
|
+
or end_cpp != f"({end})"
|
|
1732
|
+
or step_cpp != f"({step})"
|
|
1733
|
+
)
|
|
1521
1734
|
if end_eval is not None:
|
|
1522
1735
|
# The ``to`` expression is authored outside the loop-binder scope,
|
|
1523
1736
|
# but its refresh executes inside the generated C++ ``for`` where
|
|
1524
1737
|
# the binder would shadow a same-named outer member/parameter. A
|
|
1525
1738
|
# pre-binder lambda preserves the authored lexical binding while
|
|
1526
1739
|
# still reevaluating the expression after every iteration.
|
|
1527
|
-
lines.append(f"{pad}auto {end_eval} = [&]() {{ return
|
|
1528
|
-
lines.append(f"{pad}int {s_var} =
|
|
1529
|
-
end_expr = f"{end_eval}()" if end_eval is not None else
|
|
1740
|
+
lines.append(f"{pad}auto {end_eval} = [&]() {{ return {end_cpp}; }};")
|
|
1741
|
+
lines.append(f"{pad}int {s_var} = {start_cpp};")
|
|
1742
|
+
end_expr = f"{end_eval}()" if end_eval is not None else end_cpp
|
|
1530
1743
|
lines.append(f"{pad}int {e_var} = {end_expr};")
|
|
1531
|
-
lines.append(f"{pad}int {step_var} =
|
|
1532
|
-
|
|
1744
|
+
lines.append(f"{pad}int {step_var} = {step_cpp};")
|
|
1745
|
+
if na_capable:
|
|
1746
|
+
# ``-na<int>()`` would overflow; leave an na step alone and let the
|
|
1747
|
+
# loop condition reject it.
|
|
1748
|
+
lines.append(
|
|
1749
|
+
f"{pad}if (!is_na({step_var}) && {step_var} < 0)"
|
|
1750
|
+
f" {step_var} = -{step_var};"
|
|
1751
|
+
)
|
|
1752
|
+
else:
|
|
1753
|
+
lines.append(f"{pad}if ({step_var} < 0) {step_var} = -{step_var};")
|
|
1533
1754
|
lines.append(f"{pad}if ({step_var} == 0) {step_var} = 1;")
|
|
1534
1755
|
lines.append(f"{pad}const bool {down_var} = ({s_var} > {e_var});")
|
|
1756
|
+
na_guard = (
|
|
1757
|
+
f"!is_na({s_var}) && !is_na({e_var}) && !is_na({step_var}) && "
|
|
1758
|
+
if na_capable else ""
|
|
1759
|
+
)
|
|
1760
|
+
# Pine v6 evaluates ``to`` before every iteration; v5 (a v5 library's
|
|
1761
|
+
# body) fixes it before the first one.
|
|
1762
|
+
refresh = "" if self._pine_v5_body else f", {e_var} = {end_expr}"
|
|
1535
1763
|
lines.append(
|
|
1536
1764
|
f"{pad}for (int {var} = {s_var}; "
|
|
1765
|
+
f"{na_guard}"
|
|
1537
1766
|
f"({down_var} ? ({var} >= {e_var}) : ({var} <= {e_var})); "
|
|
1538
|
-
f"{var} += ({down_var} ? -{step_var} : {step_var})
|
|
1539
|
-
f"{
|
|
1767
|
+
f"{var} += ({down_var} ? -{step_var} : {step_var})"
|
|
1768
|
+
f"{refresh}) {{"
|
|
1540
1769
|
)
|
|
1541
1770
|
# Register the loop counter so reads of it inside the body resolve (the
|
|
1542
1771
|
# unknown-identifier guard in _visit_ident would otherwise flag it).
|
|
1543
1772
|
saved_loop = self._current_loop_vars
|
|
1544
1773
|
saved_loop_specs = self._current_loop_var_specs
|
|
1774
|
+
saved_counted = self._current_counted_loop_vars
|
|
1545
1775
|
self._current_loop_vars = set(self._current_loop_vars)
|
|
1546
1776
|
self._current_loop_var_specs = dict(self._current_loop_var_specs)
|
|
1777
|
+
self._current_counted_loop_vars = set(self._current_counted_loop_vars)
|
|
1547
1778
|
if node.var:
|
|
1548
1779
|
self._current_loop_vars.add(node.var)
|
|
1549
1780
|
self._current_loop_var_specs[node.var] = TypeSpec.primitive("int")
|
|
1781
|
+
self._current_counted_loop_vars.add(node.var)
|
|
1550
1782
|
_blk_saved = self._push_block_var_remap(node)
|
|
1551
1783
|
if node.var:
|
|
1552
1784
|
# The loop counter is a fresh primitive lexical binding. Keep it
|
|
@@ -1556,21 +1788,26 @@ class StmtVisitor:
|
|
|
1556
1788
|
self._lexical_series_bindings[node.var] = False
|
|
1557
1789
|
self._lexical_known_var_tombstones.add(node.var)
|
|
1558
1790
|
try:
|
|
1559
|
-
|
|
1560
|
-
self._visit_stmt(s, lines, indent + 1)
|
|
1791
|
+
self._emit_loop_body(node.body, lines, indent + 1, value_target)
|
|
1561
1792
|
finally:
|
|
1562
1793
|
self._pop_block_var_remap(_blk_saved)
|
|
1563
1794
|
self._current_loop_vars = saved_loop
|
|
1564
1795
|
self._current_loop_var_specs = saved_loop_specs
|
|
1796
|
+
self._current_counted_loop_vars = saved_counted
|
|
1565
1797
|
lines.append(f"{pad}}}")
|
|
1566
1798
|
|
|
1567
|
-
def _visit_for_in(self, node, lines: list[str], indent: int
|
|
1799
|
+
def _visit_for_in(self, node, lines: list[str], indent: int,
|
|
1800
|
+
value_target: tuple[str, str | None] | None = None) -> None:
|
|
1568
1801
|
pad = " " * indent
|
|
1569
1802
|
iterable = self._visit_expr(node.iterable)
|
|
1570
1803
|
saved_loop = self._current_loop_vars
|
|
1571
1804
|
saved_loop_specs = self._current_loop_var_specs
|
|
1805
|
+
saved_counted = self._current_counted_loop_vars
|
|
1572
1806
|
self._current_loop_vars = set(self._current_loop_vars)
|
|
1573
1807
|
self._current_loop_var_specs = dict(self._current_loop_var_specs)
|
|
1808
|
+
# Its binders shadow a counted loop's of the same spelling.
|
|
1809
|
+
self._current_counted_loop_vars = (
|
|
1810
|
+
self._current_counted_loop_vars - {node.var, *(node.vars or ())})
|
|
1574
1811
|
iterable_spec = self._type_spec_from_expr(node.iterable)
|
|
1575
1812
|
elem_spec = (
|
|
1576
1813
|
iterable_spec.element
|
|
@@ -1581,16 +1818,23 @@ class StmtVisitor:
|
|
|
1581
1818
|
self._current_loop_vars.add(node.var)
|
|
1582
1819
|
if elem_spec is not None:
|
|
1583
1820
|
self._current_loop_var_specs[node.var] = elem_spec
|
|
1821
|
+
else:
|
|
1822
|
+
# An outer binder's spec of the same spelling is not this one's.
|
|
1823
|
+
self._current_loop_var_specs.pop(node.var, None)
|
|
1584
1824
|
if node.vars:
|
|
1585
1825
|
tuple_specs: list[TypeSpec | None] = []
|
|
1586
1826
|
if iterable_spec is not None and iterable_spec.kind == "map":
|
|
1587
1827
|
tuple_specs = [iterable_spec.key, iterable_spec.value]
|
|
1828
|
+
elif elem_spec is not None:
|
|
1829
|
+
tuple_specs = [TypeSpec.primitive("int"), elem_spec]
|
|
1588
1830
|
for idx, v in enumerate(node.vars):
|
|
1589
1831
|
if v != "_":
|
|
1590
1832
|
self._current_loop_vars.add(v)
|
|
1591
1833
|
if (idx < len(tuple_specs)
|
|
1592
1834
|
and tuple_specs[idx] is not None):
|
|
1593
1835
|
self._current_loop_var_specs[v] = tuple_specs[idx]
|
|
1836
|
+
else:
|
|
1837
|
+
self._current_loop_var_specs.pop(v, None)
|
|
1594
1838
|
map_pair_loop = (
|
|
1595
1839
|
iterable_spec is not None
|
|
1596
1840
|
and iterable_spec.kind == "map"
|
|
@@ -1656,8 +1900,46 @@ class StmtVisitor:
|
|
|
1656
1900
|
lines.append(
|
|
1657
1901
|
f"{pad} auto {value_cpp} = {map_token}.get({key_cpp});"
|
|
1658
1902
|
)
|
|
1903
|
+
elif node.vars and elem_spec is not None and len(node.vars) == 2:
|
|
1904
|
+
# ``for [i, v] in arr``: the index and the element. A vector has
|
|
1905
|
+
# no pairs to decompose (``auto [i, v] : arr`` did not compile);
|
|
1906
|
+
# bind the array once, then index it, reading its size on every
|
|
1907
|
+
# iteration as the single-name loop's element order does.
|
|
1908
|
+
index_name, value_name = node.vars
|
|
1909
|
+
authored_names = (
|
|
1910
|
+
set(self._all_bound_names)
|
|
1911
|
+
| set(self._func_names)
|
|
1912
|
+
| set(self._udt_defs)
|
|
1913
|
+
| set(self._current_func_param_types)
|
|
1914
|
+
)
|
|
1915
|
+
occupied_names = authored_names | {
|
|
1916
|
+
self._safe_name(name) for name in authored_names
|
|
1917
|
+
}
|
|
1918
|
+
while True:
|
|
1919
|
+
fid = self._for_counter
|
|
1920
|
+
self._for_counter += 1
|
|
1921
|
+
array_token = f"__pf_array_iter_{fid}"
|
|
1922
|
+
index_token = f"__pf_array_index_{fid}"
|
|
1923
|
+
if not ({array_token, index_token} & occupied_names):
|
|
1924
|
+
break
|
|
1925
|
+
lines.append(f"{pad}auto&& {array_token} = {iterable};")
|
|
1926
|
+
lines.append(
|
|
1927
|
+
f"{pad}for (int {index_token} = 0; "
|
|
1928
|
+
f"{index_token} < (int){array_token}.size(); ++{index_token}) {{")
|
|
1929
|
+
if index_name != "_":
|
|
1930
|
+
lines.append(f"{pad} int {self._safe_name(index_name)} = {index_token};")
|
|
1931
|
+
if value_name != "_":
|
|
1932
|
+
# The element's value, not a reference: a ``std::vector<bool>``
|
|
1933
|
+
# element read through ``auto`` is a proxy that a later
|
|
1934
|
+
# ``arr.set(i, ...)`` in the body would change.
|
|
1935
|
+
lines.append(
|
|
1936
|
+
f"{pad} typename std::decay_t<decltype({array_token})>"
|
|
1937
|
+
f"::value_type {self._safe_name(value_name)} = "
|
|
1938
|
+
f"{array_token}[(size_t){index_token}];")
|
|
1659
1939
|
elif node.vars:
|
|
1660
|
-
bindings = ", ".join(
|
|
1940
|
+
bindings = ", ".join(
|
|
1941
|
+
self._safe_name(name) for name in node.vars
|
|
1942
|
+
)
|
|
1661
1943
|
lines.append(f"{pad}for (auto [{bindings}] : {iterable}) {{")
|
|
1662
1944
|
_blk_saved = self._push_block_var_remap(node)
|
|
1663
1945
|
loop_binding_names = (
|
|
@@ -1700,26 +1982,248 @@ class StmtVisitor:
|
|
|
1700
1982
|
self._lexical_series_bindings[name] = False
|
|
1701
1983
|
self._lexical_known_var_tombstones.add(name)
|
|
1702
1984
|
try:
|
|
1703
|
-
|
|
1704
|
-
self._visit_stmt(s, lines, indent + 1)
|
|
1985
|
+
self._emit_loop_body(node.body, lines, indent + 1, value_target)
|
|
1705
1986
|
finally:
|
|
1706
1987
|
self._pop_block_var_remap(_blk_saved)
|
|
1707
1988
|
lines.append(f"{pad}}}")
|
|
1708
1989
|
self._current_loop_vars = saved_loop
|
|
1709
1990
|
self._current_loop_var_specs = saved_loop_specs
|
|
1991
|
+
self._current_counted_loop_vars = saved_counted
|
|
1710
1992
|
|
|
1711
|
-
def _visit_while(self, node: WhileStmt, lines: list[str], indent: int
|
|
1993
|
+
def _visit_while(self, node: WhileStmt, lines: list[str], indent: int,
|
|
1994
|
+
value_target: tuple[str, str | None] | None = None) -> None:
|
|
1712
1995
|
pad = " " * indent
|
|
1713
|
-
cond = self.
|
|
1996
|
+
cond = self._coerce_bool_expr(
|
|
1997
|
+
self._visit_expr(node.condition), node.condition
|
|
1998
|
+
)
|
|
1714
1999
|
lines.append(f"{pad}while ({cond}) {{")
|
|
1715
2000
|
_blk_saved = self._push_block_var_remap(node)
|
|
1716
2001
|
try:
|
|
1717
|
-
|
|
1718
|
-
self._visit_stmt(s, lines, indent + 1)
|
|
2002
|
+
self._emit_loop_body(node.body, lines, indent + 1, value_target)
|
|
1719
2003
|
finally:
|
|
1720
2004
|
self._pop_block_var_remap(_blk_saved)
|
|
1721
2005
|
lines.append(f"{pad}}}")
|
|
1722
2006
|
|
|
2007
|
+
def _emit_loop_body(self, body: list, lines: list[str], indent: int,
|
|
2008
|
+
value_target: tuple[str, str | None] | None) -> None:
|
|
2009
|
+
"""A loop body; a loop used as a value assigns its last statement's
|
|
2010
|
+
value to ``value_target`` (``(target, target_cpp_type)``)."""
|
|
2011
|
+
if value_target is None:
|
|
2012
|
+
for s in body:
|
|
2013
|
+
self._visit_stmt(s, lines, indent)
|
|
2014
|
+
return
|
|
2015
|
+
target, target_cpp_type = value_target
|
|
2016
|
+
value_cpp = self._block_value_cpp_type(body, target_cpp_type)
|
|
2017
|
+
saved = getattr(self, "_unmatched_value_na", None)
|
|
2018
|
+
# TradingView: an if without else (a switch without default) is na
|
|
2019
|
+
# when no arm runs, so a loop whose body ends in one is na after an
|
|
2020
|
+
# iteration that ran none (lab tv probe pf-w2-f04_if_tails).
|
|
2021
|
+
self._unmatched_value_na = (
|
|
2022
|
+
(target, self._na_value_for_type(value_cpp))
|
|
2023
|
+
if value_cpp is not None else None
|
|
2024
|
+
)
|
|
2025
|
+
try:
|
|
2026
|
+
self._emit_body_with_assign(
|
|
2027
|
+
body, target, lines, indent, target_cpp_type=target_cpp_type,
|
|
2028
|
+
)
|
|
2029
|
+
finally:
|
|
2030
|
+
self._unmatched_value_na = saved
|
|
2031
|
+
|
|
2032
|
+
def _emit_loop_with_assign(
|
|
2033
|
+
self,
|
|
2034
|
+
node,
|
|
2035
|
+
target: str,
|
|
2036
|
+
lines: list[str],
|
|
2037
|
+
indent: int,
|
|
2038
|
+
target_cpp_type: str | None = None,
|
|
2039
|
+
reset: bool = True,
|
|
2040
|
+
) -> None:
|
|
2041
|
+
"""A loop that is a function's or an if/switch arm's last statement.
|
|
2042
|
+
|
|
2043
|
+
TradingView makes it the value its body's last statement produced on
|
|
2044
|
+
the last iteration that reached that statement -- a ``break`` or
|
|
2045
|
+
``continue`` before it keeps the previous value -- and ``na`` when
|
|
2046
|
+
none did (lab tv probes pf-w2-f04_loop_tail / f04_loop_edges).
|
|
2047
|
+
"""
|
|
2048
|
+
visit = {
|
|
2049
|
+
ForStmt: self._visit_for,
|
|
2050
|
+
ForInStmt: self._visit_for_in,
|
|
2051
|
+
WhileStmt: self._visit_while,
|
|
2052
|
+
}[type(node)]
|
|
2053
|
+
value_cpp = self._loop_value_cpp_type(node, target_cpp_type)
|
|
2054
|
+
if value_cpp is None:
|
|
2055
|
+
# No scalar or string value to leave (a drawing call, a void
|
|
2056
|
+
# call, a handle): the loop is the plain statement it always was.
|
|
2057
|
+
visit(node, lines, indent)
|
|
2058
|
+
return
|
|
2059
|
+
if reset:
|
|
2060
|
+
pad = " " * indent
|
|
2061
|
+
lines.append(f"{pad}{target} = {self._na_value_for_type(value_cpp)};")
|
|
2062
|
+
visit(node, lines, indent, value_target=(target, target_cpp_type))
|
|
2063
|
+
|
|
2064
|
+
def _unmatched_arm_na(self, target: str) -> str | None:
|
|
2065
|
+
"""The na a value ``target`` takes when an if/switch assigning it runs
|
|
2066
|
+
no arm (a loop's value, ``_visit_selection_value``), or None outside
|
|
2067
|
+
such a value position."""
|
|
2068
|
+
active = getattr(self, "_unmatched_value_na", None)
|
|
2069
|
+
if active is not None and active[0] == target:
|
|
2070
|
+
return active[1]
|
|
2071
|
+
return None
|
|
2072
|
+
|
|
2073
|
+
_UNMATCHED_NA_SLOTS = ("double", "int", "int64_t", "std::string", "bool")
|
|
2074
|
+
|
|
2075
|
+
def _visit_selection_value(
|
|
2076
|
+
self,
|
|
2077
|
+
node,
|
|
2078
|
+
target: str,
|
|
2079
|
+
lines: list[str],
|
|
2080
|
+
indent: int,
|
|
2081
|
+
target_cpp_type: str | None = None,
|
|
2082
|
+
slot_cpp_type: str | None = None,
|
|
2083
|
+
) -> None:
|
|
2084
|
+
"""An if/switch whose value a declaration, a reassignment or a
|
|
2085
|
+
function's last statement takes.
|
|
2086
|
+
|
|
2087
|
+
TradingView: an if without else, an else-if chain without a final
|
|
2088
|
+
else or a switch without default is na when no arm runs -- a numeric
|
|
2089
|
+
or string na, false for a bool -- as a function's last statement,
|
|
2090
|
+
nested in a taken arm, and as the value a global, reassigned or local
|
|
2091
|
+
variable takes, which does not keep its previous bar's value (lab tv
|
|
2092
|
+
probe pf-oi-if-tail-na). ``slot_cpp_type`` is the target's C++ type;
|
|
2093
|
+
a handle or collection slot keeps its existing lowering. An ``int``
|
|
2094
|
+
an epoch reaches is stored ``int64_t`` (``target_cpp_type``, the
|
|
2095
|
+
slot's integer width), and its na is the 64-bit sentinel: ``na<int>()``
|
|
2096
|
+
widened there is a value, not na (quirk 9).
|
|
2097
|
+
"""
|
|
2098
|
+
saved = getattr(self, "_unmatched_value_na", None)
|
|
2099
|
+
if slot_cpp_type == "int" and target_cpp_type == "int64_t":
|
|
2100
|
+
slot_cpp_type = "int64_t"
|
|
2101
|
+
if slot_cpp_type in self._UNMATCHED_NA_SLOTS:
|
|
2102
|
+
self._unmatched_value_na = (
|
|
2103
|
+
target, self._na_value_for_type(slot_cpp_type))
|
|
2104
|
+
try:
|
|
2105
|
+
self._visit_if_switch_expr(
|
|
2106
|
+
node, target, lines, indent, target_cpp_type=target_cpp_type,
|
|
2107
|
+
)
|
|
2108
|
+
finally:
|
|
2109
|
+
self._unmatched_value_na = saved
|
|
2110
|
+
|
|
2111
|
+
def _na_value_for_type(self, cpp_type: str | None) -> str:
|
|
2112
|
+
"""``na`` of a value slot: a numeric or string na, else its default."""
|
|
2113
|
+
if cpp_type is None:
|
|
2114
|
+
return "na<double>()"
|
|
2115
|
+
if cpp_type in ("double", "int", "int64_t", "std::string"):
|
|
2116
|
+
return f"na<{cpp_type}>()"
|
|
2117
|
+
return self._default_for_type(cpp_type)
|
|
2118
|
+
|
|
2119
|
+
_TAIL_SCALAR_CPP = ("double", "int", "int64_t", "bool")
|
|
2120
|
+
|
|
2121
|
+
def _tail_value_cpp_type(self, stmt) -> str | None:
|
|
2122
|
+
"""C++ type of the value a value-producing last statement leaves, or
|
|
2123
|
+
None when it is a drawing, UDT or collection handle (or unknown).
|
|
2124
|
+
|
|
2125
|
+
Judged from the statement itself -- a declaration's type hint and
|
|
2126
|
+
right-hand side, an assignment's target and right-hand side -- so the
|
|
2127
|
+
answer does not depend on whether its local has been emitted yet.
|
|
2128
|
+
"""
|
|
2129
|
+
if isinstance(stmt, ExprStmt):
|
|
2130
|
+
parts, typed = [stmt.expr], stmt.expr
|
|
2131
|
+
elif isinstance(stmt, VarDecl) and stmt.name:
|
|
2132
|
+
parts, typed = [stmt.value], stmt.value
|
|
2133
|
+
if stmt.type_hint:
|
|
2134
|
+
spec = self._type_spec_from_hint_name(stmt.type_hint)
|
|
2135
|
+
if spec is None or spec.kind != "primitive":
|
|
2136
|
+
return None
|
|
2137
|
+
return self._type_spec_to_cpp(spec)
|
|
2138
|
+
elif (isinstance(stmt, Assignment)
|
|
2139
|
+
and isinstance(stmt.target, (Identifier, MemberAccess))):
|
|
2140
|
+
parts, typed = [stmt.target, stmt.value], stmt.target
|
|
2141
|
+
else:
|
|
2142
|
+
return None
|
|
2143
|
+
for part in parts:
|
|
2144
|
+
if part is None:
|
|
2145
|
+
return None
|
|
2146
|
+
spec = self._type_spec_from_expr(part)
|
|
2147
|
+
if spec is not None and spec.kind != "primitive":
|
|
2148
|
+
return None
|
|
2149
|
+
value_cpp = self._infer_type(typed)
|
|
2150
|
+
if value_cpp in self._TAIL_SCALAR_CPP or value_cpp == "std::string":
|
|
2151
|
+
return value_cpp
|
|
2152
|
+
return None
|
|
2153
|
+
|
|
2154
|
+
def _tail_value_fits(self, stmt, slot_cpp_type: str | None) -> bool:
|
|
2155
|
+
"""Whether the value a last statement leaves fits the slot it feeds.
|
|
2156
|
+
|
|
2157
|
+
Only a scalar (numeric or bool) or a string is returned; a drawing,
|
|
2158
|
+
UDT or collection handle keeps the statement-then-default lowering,
|
|
2159
|
+
which is what every such tail compiled to before the value was
|
|
2160
|
+
returned. ``slot_cpp_type`` None is a double or string slot.
|
|
2161
|
+
"""
|
|
2162
|
+
value_cpp = self._tail_value_cpp_type(stmt)
|
|
2163
|
+
if value_cpp is None:
|
|
2164
|
+
return False
|
|
2165
|
+
if slot_cpp_type is None:
|
|
2166
|
+
return True
|
|
2167
|
+
if slot_cpp_type in self._TAIL_SCALAR_CPP:
|
|
2168
|
+
return value_cpp in self._TAIL_SCALAR_CPP
|
|
2169
|
+
return slot_cpp_type == "std::string" == value_cpp
|
|
2170
|
+
|
|
2171
|
+
def _loop_value_cpp_type(self, node, slot_cpp_type: str | None) -> str | None:
|
|
2172
|
+
"""C++ type of the value a loop leaves, when its body's last statement
|
|
2173
|
+
yields one that fits ``slot_cpp_type``; None otherwise."""
|
|
2174
|
+
return self._block_value_cpp_type(
|
|
2175
|
+
getattr(node, "body", None) or [], slot_cpp_type
|
|
2176
|
+
)
|
|
2177
|
+
|
|
2178
|
+
def _block_value_cpp_type(self, body: list, slot_cpp_type: str | None) -> str | None:
|
|
2179
|
+
"""C++ type of the value a block's last statement yields into
|
|
2180
|
+
``slot_cpp_type`` (through nested loops and if/switch arms), or None
|
|
2181
|
+
when it yields none that fits."""
|
|
2182
|
+
if not body:
|
|
2183
|
+
return None
|
|
2184
|
+
last = body[-1]
|
|
2185
|
+
if isinstance(last, (ForStmt, ForInStmt, WhileStmt)):
|
|
2186
|
+
return self._block_value_cpp_type(last.body, slot_cpp_type)
|
|
2187
|
+
if isinstance(last, IfStmt):
|
|
2188
|
+
arms = [last.body] + ([last.else_body] if last.else_body else [])
|
|
2189
|
+
elif isinstance(last, SwitchStmt):
|
|
2190
|
+
arms = [body_ for _, body_ in last.cases] + (
|
|
2191
|
+
[last.default_body] if last.default_body else [])
|
|
2192
|
+
else:
|
|
2193
|
+
arms = None
|
|
2194
|
+
if arms is not None:
|
|
2195
|
+
# Every arm must yield a fitting value of one type; a missing
|
|
2196
|
+
# else/default arm is na on its path.
|
|
2197
|
+
found = {self._block_value_cpp_type(arm, slot_cpp_type) for arm in arms}
|
|
2198
|
+
return found.pop() if len(found) == 1 and None not in found else None
|
|
2199
|
+
if self._statement_value_node(last) is None and not isinstance(last, ExprStmt):
|
|
2200
|
+
return None
|
|
2201
|
+
if isinstance(last, ExprStmt) and (
|
|
2202
|
+
self._call_is_void(last.expr) or self._is_skip_expr(last.expr)):
|
|
2203
|
+
return None
|
|
2204
|
+
if not self._tail_value_fits(last, slot_cpp_type):
|
|
2205
|
+
return None
|
|
2206
|
+
return slot_cpp_type or self._tail_value_cpp_type(last)
|
|
2207
|
+
|
|
2208
|
+
@staticmethod
|
|
2209
|
+
def _statement_value_node(stmt):
|
|
2210
|
+
"""The node whose value a value-producing statement leaves, or None.
|
|
2211
|
+
|
|
2212
|
+
TradingView makes the last statement of a function or an if/switch
|
|
2213
|
+
arm its value: ``x := e`` and ``x += e`` (every compound operator)
|
|
2214
|
+
yield the target's new value, ``obj.f := e`` the field's, and a
|
|
2215
|
+
declaration ``[var] [T] x = e`` the declared variable's (lab tv probes
|
|
2216
|
+
pf-w2-f04_tails / f04_decl_tail / f04_var_decl_tail).
|
|
2217
|
+
"""
|
|
2218
|
+
if isinstance(stmt, Assignment) and isinstance(
|
|
2219
|
+
stmt.target, (Identifier, MemberAccess)):
|
|
2220
|
+
return stmt.target
|
|
2221
|
+
if isinstance(stmt, VarDecl) and stmt.name:
|
|
2222
|
+
node = Identifier(name=stmt.name)
|
|
2223
|
+
node.loc = stmt.loc
|
|
2224
|
+
return node
|
|
2225
|
+
return None
|
|
2226
|
+
|
|
1723
2227
|
def _visit_switch(self, node: SwitchStmt, lines: list[str], indent: int) -> None:
|
|
1724
2228
|
pad = " " * indent
|
|
1725
2229
|
if node.expr:
|
|
@@ -1735,7 +2239,9 @@ class StmtVisitor:
|
|
|
1735
2239
|
else:
|
|
1736
2240
|
for i, (case_expr, case_body) in enumerate(node.cases):
|
|
1737
2241
|
prefix = "if" if i == 0 else "else if"
|
|
1738
|
-
cond = self.
|
|
2242
|
+
cond = self._coerce_bool_expr(
|
|
2243
|
+
self._visit_expr(case_expr), case_expr
|
|
2244
|
+
)
|
|
1739
2245
|
lines.append(f"{pad}{prefix} ({cond}) {{")
|
|
1740
2246
|
self._visit_block_statements(case_body, lines, indent + 1)
|
|
1741
2247
|
lines.append(f"{pad}}}")
|
|
@@ -1777,9 +2283,13 @@ class StmtVisitor:
|
|
|
1777
2283
|
if self._call_is_void(stmt.expr):
|
|
1778
2284
|
self._visit_stmt(stmt, lines, indent)
|
|
1779
2285
|
return
|
|
1780
|
-
cpp = self.
|
|
2286
|
+
cpp = self._coerce_int_slot(
|
|
2287
|
+
self._visit_rhs_value(
|
|
2288
|
+
stmt.expr,
|
|
2289
|
+
target_cpp_type=target_cpp_type,
|
|
2290
|
+
),
|
|
1781
2291
|
stmt.expr,
|
|
1782
|
-
target_cpp_type
|
|
2292
|
+
self._int_slot_cpp_type(None, target_cpp_type),
|
|
1783
2293
|
)
|
|
1784
2294
|
pad = " " * indent
|
|
1785
2295
|
lines.append(f"{pad}{target} = {cpp};")
|
|
@@ -1800,6 +2310,26 @@ class StmtVisitor:
|
|
|
1800
2310
|
indent,
|
|
1801
2311
|
target_cpp_type=target_cpp_type,
|
|
1802
2312
|
)
|
|
2313
|
+
elif (self._statement_value_node(stmt) is not None
|
|
2314
|
+
and self._tail_value_fits(stmt, target_cpp_type)):
|
|
2315
|
+
# ``y := v * 5`` / ``y += v`` / ``float y = v`` as the
|
|
2316
|
+
# value: run the statement, then read what it left.
|
|
2317
|
+
self._visit_stmt(stmt, lines, indent)
|
|
2318
|
+
value = self._statement_value_node(stmt)
|
|
2319
|
+
cpp = self._coerce_int_slot(
|
|
2320
|
+
self._visit_rhs_value(
|
|
2321
|
+
value, target_cpp_type=target_cpp_type,
|
|
2322
|
+
),
|
|
2323
|
+
value,
|
|
2324
|
+
self._int_slot_cpp_type(None, target_cpp_type),
|
|
2325
|
+
)
|
|
2326
|
+
pad = " " * indent
|
|
2327
|
+
lines.append(f"{pad}{target} = {cpp};")
|
|
2328
|
+
elif isinstance(stmt, (ForStmt, ForInStmt, WhileStmt)):
|
|
2329
|
+
self._emit_loop_with_assign(
|
|
2330
|
+
stmt, target, lines, indent,
|
|
2331
|
+
target_cpp_type=target_cpp_type,
|
|
2332
|
+
)
|
|
1803
2333
|
else:
|
|
1804
2334
|
self._visit_stmt(stmt, lines, indent)
|
|
1805
2335
|
else:
|
|
@@ -1828,7 +2358,9 @@ class StmtVisitor:
|
|
|
1828
2358
|
lines.append(f"{pad}}}")
|
|
1829
2359
|
|
|
1830
2360
|
if isinstance(node, IfStmt):
|
|
1831
|
-
cond = self.
|
|
2361
|
+
cond = self._coerce_bool_expr(
|
|
2362
|
+
self._visit_expr(node.condition), node.condition
|
|
2363
|
+
)
|
|
1832
2364
|
lines.append(f"{pad}if ({cond}) {{")
|
|
1833
2365
|
self._emit_block_with_assign(
|
|
1834
2366
|
node.body,
|
|
@@ -1864,6 +2396,10 @@ class StmtVisitor:
|
|
|
1864
2396
|
# non-var globals and reassignments: retaining the prior bar's
|
|
1865
2397
|
# map/matrix ID would turn the expression into implicit state.
|
|
1866
2398
|
emit_implicit_na_fallback()
|
|
2399
|
+
elif self._unmatched_arm_na(target) is not None:
|
|
2400
|
+
lines.append(f"{pad}else {{")
|
|
2401
|
+
lines.append(f"{pad} {target} = {self._unmatched_arm_na(target)};")
|
|
2402
|
+
lines.append(f"{pad}}}")
|
|
1867
2403
|
elif isinstance(node, SwitchStmt):
|
|
1868
2404
|
if node.expr:
|
|
1869
2405
|
expr_var = f"__switch_val_{self._switch_counter}"
|
|
@@ -1884,7 +2420,9 @@ class StmtVisitor:
|
|
|
1884
2420
|
else:
|
|
1885
2421
|
for i, (case_expr, case_body) in enumerate(node.cases):
|
|
1886
2422
|
prefix = "if" if i == 0 else "else if"
|
|
1887
|
-
cond = self.
|
|
2423
|
+
cond = self._coerce_bool_expr(
|
|
2424
|
+
self._visit_expr(case_expr), case_expr
|
|
2425
|
+
)
|
|
1888
2426
|
lines.append(f"{pad}{prefix} ({cond}) {{")
|
|
1889
2427
|
self._emit_block_with_assign(
|
|
1890
2428
|
case_body,
|
|
@@ -1920,3 +2458,11 @@ class StmtVisitor:
|
|
|
1920
2458
|
lines.append(
|
|
1921
2459
|
f"{pad}{target} = {target_cpp_type}{{}};"
|
|
1922
2460
|
)
|
|
2461
|
+
elif self._unmatched_arm_na(target) is not None:
|
|
2462
|
+
na_value = self._unmatched_arm_na(target)
|
|
2463
|
+
if node.cases:
|
|
2464
|
+
lines.append(f"{pad}else {{")
|
|
2465
|
+
lines.append(f"{pad} {target} = {na_value};")
|
|
2466
|
+
lines.append(f"{pad}}}")
|
|
2467
|
+
else:
|
|
2468
|
+
lines.append(f"{pad}{target} = {na_value};")
|